diff --git a/CHANGELOG.md b/CHANGELOG.md index 641568de..3d792e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,75 @@ ## WLED changelog +#### Build 2307130 +- larger `oappend()` stack buffer (3.5k) for ESP32 +- Preset cycle bugfix (#3262) +- Rotary encoder ALT fix for large LED count (#3276) +- effect updates (2D Plasmaball), `blur()` speedup +- On/Off toggle from nodes view (may show unknow device type on older versions) (#3291) +- various fixes and improvements (ABL, crashes when changing presets with different segments) + +#### Build 2306270 +- ESP-NOW remote support (#3237) +- Pixel Magic tool (display pixel art) (#3249) +- Websocket (peek) fallback when connection cannot be established, WS retries (#3267) +- Add WiFi network scan RPC command to Improv Serial (#3271) +- Longer (custom option available) segment name for ESP32 +- various fixes and improvements + +#### Build 2306210 +- 0.14.0-b3 release +- respect global I2C in all usermods (no local initilaisation of I2C bus) +- Multi relay usermod compile-time enabled option (-D MULTI_RELAY_ENABLED=true|false) + +#### Build 2306180 +- Added client-side option for applying effect defaults from metadata +- Improved ESP8266 stability by reducing WebSocket response resends +- Updated ESP8266 core to 3.1.2 + +#### Build 2306141 +- Lissajous improvements +- Scrolling Text improvements (leading 0) + +#### Build 2306140 +- Add settings PIN (un)locking to JSON post API + +#### Build 2306130 +- Bumped version to 0.14-b3 (beta 3) +- added pin dropdowns in LED preferences (not for LED pins) and usermods +- introduced (unused ATM) NeoGammaWLEDMethod class +- Reverse proxy support +- PCF8754 support for Rotary encoder (requires wiring INT pin to ESP GPIO) +- Rely on global I2C pins for usermods (breaking change) +- various fixes and enhancements + +#### 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/package-lock.json b/package-lock.json index 2ec2d887..c4fe9b1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.14.0-b2", + "version": "0.14.0-b3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1409,9 +1409,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "semver-diff": { "version": "2.1.0", diff --git a/package.json b/package.json index 5c4cfde4..d57c87d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.14.0-b2", + "version": "0.14.0-b3", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/platformio.ini b/platformio.ini index 5bd8a699..d3b71d3c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ # ------------------------------------------------------------------------------ # CI binaries -;; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth # ESP32 variant builds are temporarily excluded from CI due to toolchain issues on the GitHub Actions Linux environment +; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth # ESP32 variant builds are temporarily excluded from CI due to toolchain issues on the GitHub Actions Linux environment default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB # Release binaries @@ -40,6 +40,8 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_ ; default_envs = esp32dev_qio80 ; default_envs = esp32_eth_ota1mapp ; default_envs = esp32s2_saola +; default_envs = esp32c3dev +; default_envs = lolin_s2_mini src_dir = ./wled00 data_dir = ./wled00/data @@ -60,13 +62,14 @@ arduino_core_2_7_4 = espressif8266@2.6.2 arduino_core_3_0_0 = espressif8266@3.0.0 arduino_core_3_2_0 = espressif8266@3.2.0 arduino_core_4_1_0 = espressif8266@4.1.0 +arduino_core_3_1_2 = espressif8266@4.2.0 # Development platforms arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/stage # Platform to use for ESP8266 -platform_wled_default = ${common.arduino_core_4_1_0} +platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization #platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 platform_packages = platformio/framework-arduinoespressif8266 @@ -178,14 +181,12 @@ lib_deps = https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.7 #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI - #For use SSD1306 OLED display uncomment following - #U8g2@~2.28.8 - #U8g2@~2.32.10 - #For Dallas sensor uncomment following 2 lines - #OneWire@~2.3.5 - #milesburton/DallasTemperature@^3.9.0 + #For compatible OLED display uncomment following + #U8g2 #@ ~2.33.15 + #For Dallas sensor uncomment following + #OneWire @ ~2.3.7 #For BME280 sensor uncomment following - #BME280@~3.0.0 + #BME280 @ ~3.0.0 ; adafruit/Adafruit BMP280 Library @ 2.1.0 ; adafruit/Adafruit CCS811 Library @ 1.0.4 ; adafruit/Adafruit Si7021 Library @ 1.4.0 @@ -461,8 +462,8 @@ board = esp32-c3-devkitm-1 board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv build_flags = ${common.build_flags} ${esp32c3.build_flags} #-D WLED_RELEASE_NAME=ESP32-C3 -D WLED_WATCHDOG_TIMEOUT=0 - ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB - -DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip + -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB + ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} @@ -574,10 +575,10 @@ platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv -build_unflags = ${common.build_unflags} -DARDUINO_USB_CDC_ON_BOOT=1 +build_unflags = ${common.build_unflags} #-DARDUINO_USB_CDC_ON_BOOT=1 build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME=LolinS2 -DBOARD_HAS_PSRAM - -DARDUINO_USB_CDC_ON_BOOT=0 + -DARDUINO_USB_CDC_ON_BOOT=1 # try disabling and enabling unflag above in case of board-specific issues, will disable Serial -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DLOLIN_WIFI_FIX ; seems to work much better with this 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/tools/cdata.js b/tools/cdata.js index 97000436..90619ba6 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -222,6 +222,7 @@ writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index'); writeHtmlGzipped("wled00/data/simple.htm", "wled00/html_simple.h", 'simple'); writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart'); writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal'); +writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic'); /* writeChunks( "wled00/data", @@ -389,12 +390,6 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; method: "gzip", filter: "html-minify", }, - { - file: "liveviewws.htm", - name: "PAGE_liveviewws", - method: "gzip", - filter: "html-minify", - }, { file: "liveviewws2D.htm", name: "PAGE_liveviewws2D", diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h index b65332bc..5e597d01 100644 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -8,7 +8,6 @@ #pragma once #include "wled.h" -#include #include // the max frequency to check photoresistor, 10 seconds @@ -56,15 +55,6 @@ private: static const char _offset[]; static const char _HomeAssistantDiscovery[]; - // set the default pins based on the architecture, these get overridden by Usermod menu settings - #ifdef ARDUINO_ARCH_ESP32 // ESP32 boards - #define HW_PIN_SCL 22 - #define HW_PIN_SDA 21 - #else // ESP8266 boards - #define HW_PIN_SCL 5 - #define HW_PIN_SDA 4 - #endif - int8_t ioPin[2] = {HW_PIN_SCL, HW_PIN_SDA}; // I2C pins: SCL, SDA...defaults to Arch hardware pins but overridden at setup() bool initDone = false; bool sensorFound = false; @@ -123,14 +113,7 @@ private: public: void setup() { - bool HW_Pins_Used = (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA); // note whether architecture-based hardware SCL/SDA pins used - PinOwner po = PinOwner::UM_BH1750; // defaults to being pinowner for SCL/SDA pins - PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; // allocate pins - if (HW_Pins_Used) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - if (!pinManager.allocateMultiplePins(pins, 2, po)) return; - - Wire.begin(ioPin[1], ioPin[0]); - + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } sensorFound = lightMeter.begin(); initDone = true; } @@ -190,7 +173,9 @@ public: user = root.createNestedObject(F("u")); JsonArray lux_json = user.createNestedArray(F("Luminance")); - if (!sensorFound) { + if (!enabled) { + lux_json.add(F("disabled")); + } else if (!sensorFound) { // if no sensor lux_json.add(F("BH1750 ")); lux_json.add(F("Not Found")); @@ -216,9 +201,6 @@ public: top[FPSTR(_minReadInterval)] = minReadingInterval; top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; top[FPSTR(_offset)] = offset; - JsonArray io_pin = top.createNestedArray(F("pin")); - for (byte i=0; i<2; i++) io_pin.add(ioPin[i]); - top[F("help4Pins")] = F("SCL,SDA"); // help for Settings page DEBUG_PRINTLN(F("BH1750 config saved.")); } @@ -226,8 +208,6 @@ public: // called before setup() to populate properties from values stored in cfg.json bool readFromConfig(JsonObject &root) { - int8_t newPin[2]; for (byte i=0; i<2; i++) newPin[i] = ioPin[i]; // prepare to note changed pins - // we look for JSON object. JsonObject top = root[FPSTR(_name)]; if (top.isNull()) @@ -244,27 +224,12 @@ public: configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); - for (byte i=0; i<2; i++) configComplete &= getJsonValue(top[F("pin")][i], newPin[i], ioPin[i]); DEBUG_PRINT(FPSTR(_name)); if (!initDone) { - // first run: reading from cfg.json - for (byte i=0; i<2; i++) ioPin[i] = newPin[i]; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - bool pinsChanged = false; - for (byte i=0; i<2; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } // check if any pins changed - if (pinsChanged) { //if pins changed, deallocate old pins and allocate new ones - PinOwner po = PinOwner::UM_BH1750; - if (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 2, po); // deallocate pins - for (byte i=0; i<2; i++) ioPin[i] = newPin[i]; - setup(); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[F("pin")].isNull(); } return configComplete; diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/usermod_bme280.h index e643e62b..c7d25ec1 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/usermod_bme280.h @@ -9,7 +9,6 @@ #include "wled.h" #include -#include #include // BME280 sensor #include // BME280 extended measurements @@ -34,7 +33,6 @@ private: #ifdef ESP8266 //uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 #endif - int8_t ioPin[2] = {i2c_scl, i2c_sda}; // I2C pins: SCL, SDA...defaults to Arch hardware pins but overridden at setup() bool initDone = false; // BME280 sensor settings @@ -186,14 +184,8 @@ private: public: void setup() { - bool HW_Pins_Used = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); // note whether architecture-based hardware SCL/SDA pins used - PinOwner po = PinOwner::UM_BME280; // defaults to being pinowner for SCL/SDA pins - PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; // allocate pins - if (HW_Pins_Used) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - if (!pinManager.allocateMultiplePins(pins, 2, po)) { sensorType=0; return; } + if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; } - Wire.begin(ioPin[1], ioPin[0]); - if (!bme.begin()) { sensorType = 0; @@ -415,9 +407,6 @@ public: top[F("PublishAlways")] = PublishAlways; top[F("UseCelsius")] = UseCelsius; top[F("HomeAssistantDiscovery")] = HomeAssistantDiscovery; - JsonArray io_pin = top.createNestedArray(F("pin")); - for (byte i=0; i<2; i++) io_pin.add(ioPin[i]); - top[F("help4Pins")] = F("SCL,SDA"); // help for Settings page DEBUG_PRINTLN(F("BME280 config saved.")); } @@ -427,8 +416,6 @@ public: // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) - int8_t newPin[2]; for (byte i=0; i<2; i++) newPin[i] = ioPin[i]; // prepare to note changed pins - JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(F(_name)); @@ -447,27 +434,14 @@ public: configComplete &= getJsonValue(top[F("PublishAlways")], PublishAlways, false); configComplete &= getJsonValue(top[F("UseCelsius")], UseCelsius, true); configComplete &= getJsonValue(top[F("HomeAssistantDiscovery")], HomeAssistantDiscovery, false); - for (byte i=0; i<2; i++) configComplete &= getJsonValue(top[F("pin")][i], newPin[i], ioPin[i]); DEBUG_PRINT(FPSTR(_name)); if (!initDone) { // first run: reading from cfg.json - for (byte i=0; i<2; i++) ioPin[i] = newPin[i]; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page - bool pinsChanged = false; - for (byte i=0; i<2; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } // check if any pins changed - if (pinsChanged) { //if pins changed, deallocate old pins and allocate new ones - PinOwner po = PinOwner::UM_BME280; - if (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 2, po); // deallocate pins - for (byte i=0; i<2; i++) ioPin[i] = newPin[i]; - setup(); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[F("pin")].isNull(); } return configComplete; 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/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 7903fc9e..8a4b9a60 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -20,7 +20,7 @@ * This usermod handles PIR sensor states. * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. - * + * Maintained by: @blazoncek * * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality @@ -38,21 +38,21 @@ public: ~PIRsensorSwitch() {} //Enable/Disable the PIR sensor - void EnablePIRsensor(bool en) { enabled = en; } + inline void EnablePIRsensor(bool en) { enabled = en; } // Get PIR sensor enabled/disabled state - bool PIRsensorEnabled() { return enabled; } + inline bool PIRsensorEnabled() { return enabled; } private: byte prevPreset = 0; byte prevPlaylist = 0; - uint32_t offTimerStart = 0; // off timer start time + volatile unsigned long offTimerStart = 0; // off timer start time + volatile bool PIRtriggered = false; // did PIR trigger? byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // notification mode for stateUpdated(): CALL_MODE_NO_NOTIFY or CALL_MODE_DIRECT_CHANGE byte sensorPinState = LOW; // current PIR sensor pin state bool initDone = false; // status of initialization - bool PIRtriggered = false; unsigned long lastLoop = 0; // configurable parameters @@ -66,6 +66,7 @@ private: // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR) bool m_offOnly = false; bool m_offMode = offMode; + bool m_override = false; // Home Assistant bool HomeAssistantDiscovery = false; // is HA discovery turned on @@ -81,173 +82,33 @@ private: static const char _offOnly[]; static const char _haDiscovery[]; static const char _notify[]; + static const char _override[]; /** * check if it is daytime * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime */ - bool isDayTime() { - updateLocalTime(); - uint8_t hr = hour(localTime); - uint8_t mi = minute(localTime); - - if (sunrise && sunset) { - if (hour(sunrise)
hr) { - return true; - } else { - if (hour(sunrise)==hr && minute(sunrise)mi) { - return true; - } - } - } - return false; - } + static bool isDayTime(); /** * switch strip on/off */ - void switchStrip(bool switchOn) - { - if (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; //if lights on and off only, do nothing - if (PIRtriggered && switchOn) return; //if already on and triggered before, do nothing - PIRtriggered = switchOn; - DEBUG_PRINT(F("PIR: strip=")); DEBUG_PRINTLN(switchOn?"on":"off"); - if (switchOn) { - if (m_onPreset) { - if (currentPlaylist>0 && !offMode) { - prevPlaylist = currentPlaylist; - unloadPlaylist(); - } else if (currentPreset>0 && !offMode) { - prevPreset = currentPreset; - } else { - saveTemporaryPreset(); - prevPlaylist = 0; - prevPreset = 255; - } - applyPreset(m_onPreset, NotifyUpdateMode); - return; - } - // preset not assigned - if (bri == 0) { - bri = briLast; - stateUpdated(NotifyUpdateMode); - } - } else { - if (m_offPreset) { - applyPreset(m_offPreset, NotifyUpdateMode); - return; - } else if (prevPlaylist) { - if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPlaylist, NotifyUpdateMode); - prevPlaylist = 0; - return; - } else if (prevPreset) { - if (prevPreset<255) { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPreset, NotifyUpdateMode); } - else { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyTemporaryPreset(); } - prevPreset = 0; - return; - } - // preset not assigned - if (bri != 0) { - briLast = bri; - bri = 0; - stateUpdated(NotifyUpdateMode); - } - } - } - - void publishMqtt(const char* state) - { - #ifndef WLED_DISABLE_MQTT - //Check if MQTT Connected, otherwise it will crash the 8266 - if (WLED_MQTT_CONNECTED) { - char subuf[64]; - strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/motion")); - mqtt->publish(subuf, 0, false, state); - } - #endif - } + void switchStrip(bool switchOn); + void publishMqtt(const char* state); // Create an MQTT Binary Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. - void publishHomeAssistantAutodiscovery() - { - #ifndef WLED_DISABLE_MQTT - if (WLED_MQTT_CONNECTED) { - StaticJsonDocument<600> doc; - char uid[24], json_str[1024], buf[128]; - - sprintf_P(buf, PSTR("%s Motion"), serverDescription); //max length: 33 + 7 = 40 - doc[F("name")] = buf; - sprintf_P(buf, PSTR("%s/motion"), mqttDeviceTopic); //max length: 33 + 7 = 40 - doc[F("stat_t")] = buf; - doc[F("pl_on")] = "on"; - doc[F("pl_off")] = "off"; - sprintf_P(uid, PSTR("%s_motion"), escapedMac.c_str()); - doc[F("uniq_id")] = uid; - doc[F("dev_cla")] = F("motion"); - doc[F("exp_aft")] = 1800; - - JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device - device[F("name")] = serverDescription; - device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; - device[F("mf")] = "WLED"; - device[F("mdl")] = F("FOSS"); - device[F("sw")] = versionString; - - sprintf_P(buf, PSTR("homeassistant/binary_sensor/%s/config"), uid); - DEBUG_PRINTLN(buf); - size_t payload_size = serializeJson(doc, json_str); - DEBUG_PRINTLN(json_str); - - mqtt->publish(buf, 0, true, json_str, payload_size); // do we really need to retain? - } - #endif - } + void publishHomeAssistantAutodiscovery(); /** * Read and update PIR sensor state. * Initilize/reset switch off timer */ - bool updatePIRsensorState() - { - bool pinState = digitalRead(PIRsensorPin); - if (pinState != sensorPinState) { - sensorPinState = pinState; // change previous state - - if (sensorPinState == HIGH) { - offTimerStart = 0; - if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); - else if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); - publishMqtt("on"); - } else { - // start switch off timer - offTimerStart = millis(); - if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); - } - return true; - } - return false; - } + bool updatePIRsensorState(); /** * switch off the strip if the delay has elapsed */ - bool handleOffTimer() - { - if (offTimerStart > 0 && millis() - offTimerStart > m_switchOffDelay) { - offTimerStart = 0; - if (enabled == true) { - if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()) || PIRtriggered)) switchStrip(false); - else if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); - publishMqtt("off"); - } - return true; - } - return false; - } + bool handleOffTimer(); public: //Functions called by WLED @@ -256,186 +117,57 @@ public: * 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() - { - if (enabled) { - // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (PIRsensorPin >= 0 && pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { - // PIR Sensor mode INPUT_PULLUP - pinMode(PIRsensorPin, INPUT_PULLUP); - sensorPinState = digitalRead(PIRsensorPin); - } else { - if (PIRsensorPin >= 0) { - DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); - } - PIRsensorPin = -1; // allocation failed - enabled = false; - } - } - initDone = true; - } + void setup(); /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ - void connected() - { - } + //void connected(); /** * onMqttConnect() is called when MQTT connection is established */ - void onMqttConnect(bool sessionPresent) { - if (HomeAssistantDiscovery) { - publishHomeAssistantAutodiscovery(); - } - } + void onMqttConnect(bool sessionPresent); /** * loop() is called continuously. Here you can check for events, read sensors, etc. */ - void loop() - { - // only check sensors 4x/s - if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; - lastLoop = millis(); - - if (!updatePIRsensorState()) { - handleOffTimer(); - } - } + void loop(); /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * * Add PIR sensor state and switch off timer duration to jsoninfo */ - void addToJsonInfo(JsonObject &root) - { - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray(FPSTR(_name)); - - String uiDomString; - if (enabled) { - if (offTimerStart > 0) - { - uiDomString = ""; - unsigned int offSeconds = (m_switchOffDelay - (millis() - offTimerStart)) / 1000; - if (offSeconds >= 3600) - { - uiDomString += (offSeconds / 3600); - uiDomString += F("h "); - offSeconds %= 3600; - } - if (offSeconds >= 60) - { - uiDomString += (offSeconds / 60); - offSeconds %= 60; - } - else if (uiDomString.length() > 0) - { - uiDomString += 0; - } - if (uiDomString.length() > 0) - { - uiDomString += F("min "); - } - uiDomString += (offSeconds); - infoArr.add(uiDomString + F("s")); - } else { - infoArr.add(sensorPinState ? F("sensor on") : F("inactive")); - } - } else { - infoArr.add(F("disabled")); - } - - uiDomString = F(" "); - infoArr.add(uiDomString); - - JsonObject sensor = root[F("sensor")]; - if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); - sensor[F("motion")] = sensorPinState || offTimerStart>0 ? true : false; - } + void addToJsonInfo(JsonObject &root); /** * onStateChanged() is used to detect WLED state change */ - void onStateChange(uint8_t mode) { - if (!initDone) return; - DEBUG_PRINT(F("PIR: offTimerStart=")); DEBUG_PRINTLN(offTimerStart); - if (PIRtriggered && offTimerStart) { - // checking PIRtriggered and offTimerStart will prevent cancellation upon On trigger - DEBUG_PRINTLN(F("PIR: Canceled.")); - offTimerStart = 0; - PIRtriggered = false; - } - } + void onStateChange(uint8_t mode); /** * 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); /** * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - - void readFromJsonState(JsonObject &root) - { - if (!initDone) return; // prevent crash on boot applyPreset() - JsonObject usermod = root[FPSTR(_name)]; - if (!usermod.isNull()) { - if (usermod[FPSTR(_enabled)].is()) { - enabled = usermod[FPSTR(_enabled)].as(); - } - } - } - + void readFromJsonState(JsonObject &root); /** * provide the changeable values */ - void addToConfig(JsonObject &root) - { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; - top["pin"] = PIRsensorPin; - top[FPSTR(_onPreset)] = m_onPreset; - top[FPSTR(_offPreset)] = m_offPreset; - top[FPSTR(_nightTime)] = m_nightTimeOnly; - top[FPSTR(_mqttOnly)] = m_mqttOnly; - top[FPSTR(_offOnly)] = m_offOnly; - top[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; - top[FPSTR(_notify)] = (NotifyUpdateMode != CALL_MODE_NO_NOTIFY); - DEBUG_PRINTLN(F("PIR config saved.")); - } + void addToConfig(JsonObject &root); - void appendConfigData() - { - oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('PIRsensorSwitch:notifications',1,'Periodic WS updates');")); // 0 is field type, 1 is actual field - } + /** + * provide UI information and allow extending UI options + */ + void appendConfigData(); /** * restore the changeable values @@ -443,72 +175,13 @@ public: * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ - bool readFromConfig(JsonObject &root) - { - bool oldEnabled = enabled; - int8_t oldPin = PIRsensorPin; - - DEBUG_PRINT(FPSTR(_name)); - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - PIRsensorPin = top["pin"] | PIRsensorPin; - - enabled = top[FPSTR(_enabled)] | enabled; - - m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; - - m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; - m_onPreset = max(0,min(250,(int)m_onPreset)); - m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; - m_offPreset = max(0,min(250,(int)m_offPreset)); - - m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; - m_mqttOnly = top[FPSTR(_mqttOnly)] | m_mqttOnly; - m_offOnly = top[FPSTR(_offOnly)] | m_offOnly; - HomeAssistantDiscovery = top[FPSTR(_haDiscovery)] | HomeAssistantDiscovery; - - NotifyUpdateMode = top[FPSTR(_notify)] ? CALL_MODE_DIRECT_CHANGE : CALL_MODE_NO_NOTIFY; - - if (!initDone) { - // reading config prior to setup() - DEBUG_PRINTLN(F(" config loaded.")); - } else { - if (oldPin != PIRsensorPin || oldEnabled != enabled) { - // check if pin is OK - if (oldPin != PIRsensorPin && oldPin >= 0) { - // if we are changing pin in settings page - // deallocate old pin - pinManager.deallocatePin(oldPin, PinOwner::UM_PIR); - if (pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { - pinMode(PIRsensorPin, INPUT_PULLUP); - } else { - // allocation failed - PIRsensorPin = -1; - enabled = false; - } - } - if (enabled) { - sensorPinState = digitalRead(PIRsensorPin); - } - } - DEBUG_PRINTLN(F(" config (re)loaded.")); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_haDiscovery)].isNull(); - } + bool readFromConfig(JsonObject &root); /** * 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_PIRSWITCH; - } + uint16_t getId() { return USERMOD_ID_PIRSWITCH; } }; // strings to reduce flash memory usage (used more than twice) @@ -522,3 +195,359 @@ const char PIRsensorSwitch::_mqttOnly[] PROGMEM = "mqtt-only"; const char PIRsensorSwitch::_offOnly[] PROGMEM = "off-only"; const char PIRsensorSwitch::_haDiscovery[] PROGMEM = "HA-discovery"; const char PIRsensorSwitch::_notify[] PROGMEM = "notifications"; +const char PIRsensorSwitch::_override[] PROGMEM = "override"; + +bool PIRsensorSwitch::isDayTime() { + updateLocalTime(); + uint8_t hr = hour(localTime); + uint8_t mi = minute(localTime); + + if (sunrise && sunset) { + if (hour(sunrise)
hr) { + return true; + } else { + if (hour(sunrise)==hr && minute(sunrise)mi) { + return true; + } + } + } + return false; +} + +void PIRsensorSwitch::switchStrip(bool switchOn) +{ + if (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; //if lights on and off only, do nothing + if (PIRtriggered && switchOn) return; //if already on and triggered before, do nothing + PIRtriggered = switchOn; + DEBUG_PRINT(F("PIR: strip=")); DEBUG_PRINTLN(switchOn?"on":"off"); + if (switchOn) { + if (m_onPreset) { + if (currentPlaylist>0 && !offMode) { + prevPlaylist = currentPlaylist; + unloadPlaylist(); + } else if (currentPreset>0 && !offMode) { + prevPreset = currentPreset; + } else { + saveTemporaryPreset(); + prevPlaylist = 0; + prevPreset = 255; + } + applyPreset(m_onPreset, NotifyUpdateMode); + return; + } + // preset not assigned + if (bri == 0) { + bri = briLast; + stateUpdated(NotifyUpdateMode); + } + } else { + if (m_offPreset) { + applyPreset(m_offPreset, NotifyUpdateMode); + return; + } else if (prevPlaylist) { + if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPlaylist, NotifyUpdateMode); + prevPlaylist = 0; + return; + } else if (prevPreset) { + if (prevPreset<255) { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPreset, NotifyUpdateMode); } + else { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyTemporaryPreset(); } + prevPreset = 0; + return; + } + // preset not assigned + if (bri != 0) { + briLast = bri; + bri = 0; + stateUpdated(NotifyUpdateMode); + } + } +} + +void PIRsensorSwitch::publishMqtt(const char* state) +{ +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED) { + char buf[64]; + sprintf_P(buf, PSTR("%s/motion"), mqttDeviceTopic); //max length: 33 + 7 = 40 + mqtt->publish(buf, 0, false, state); + } +#endif +} + +void PIRsensorSwitch::publishHomeAssistantAutodiscovery() +{ +#ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) { + StaticJsonDocument<600> doc; + char uid[24], json_str[1024], buf[128]; + + sprintf_P(buf, PSTR("%s Motion"), serverDescription); //max length: 33 + 7 = 40 + doc[F("name")] = buf; + sprintf_P(buf, PSTR("%s/motion"), mqttDeviceTopic); //max length: 33 + 7 = 40 + doc[F("stat_t")] = buf; + doc[F("pl_on")] = "on"; + doc[F("pl_off")] = "off"; + sprintf_P(uid, PSTR("%s_motion"), escapedMac.c_str()); + doc[F("uniq_id")] = uid; + doc[F("dev_cla")] = F("motion"); + doc[F("exp_aft")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; + device[F("mf")] = "WLED"; + device[F("mdl")] = F("FOSS"); + device[F("sw")] = versionString; + + sprintf_P(buf, PSTR("homeassistant/binary_sensor/%s/config"), uid); + DEBUG_PRINTLN(buf); + size_t payload_size = serializeJson(doc, json_str); + DEBUG_PRINTLN(json_str); + + mqtt->publish(buf, 0, true, json_str, payload_size); // do we really need to retain? + } +#endif +} + +bool PIRsensorSwitch::updatePIRsensorState() +{ + bool pinState = digitalRead(PIRsensorPin); + if (pinState != sensorPinState) { + sensorPinState = pinState; // change previous state + + if (sensorPinState == HIGH) { + offTimerStart = 0; + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); + else if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); + publishMqtt("on"); + } else { + // start switch off timer + offTimerStart = millis(); + if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); + } + return true; + } + return false; +} + +bool PIRsensorSwitch::handleOffTimer() +{ + if (offTimerStart > 0 && millis() - offTimerStart > m_switchOffDelay) { + offTimerStart = 0; + if (enabled == true) { + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()) || PIRtriggered)) switchStrip(false); + else if (NotifyUpdateMode != CALL_MODE_NO_NOTIFY) updateInterfaces(CALL_MODE_WS_SEND); + publishMqtt("off"); + } + return true; + } + return false; +} + +//Functions called by WLED + +void PIRsensorSwitch::setup() +{ + if (enabled) { + // pin retrieved from cfg.json (readFromConfig()) prior to running setup() + if (PIRsensorPin >= 0 && pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { + // PIR Sensor mode INPUT_PULLUP + pinMode(PIRsensorPin, INPUT_PULLUP); + sensorPinState = digitalRead(PIRsensorPin); + } else { + if (PIRsensorPin >= 0) { + DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); + } + PIRsensorPin = -1; // allocation failed + enabled = false; + } + } + initDone = true; +} + +void PIRsensorSwitch::onMqttConnect(bool sessionPresent) +{ + if (HomeAssistantDiscovery) { + publishHomeAssistantAutodiscovery(); + } +} + +void PIRsensorSwitch::loop() +{ + // only check sensors 4x/s + if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; + lastLoop = millis(); + + if (!updatePIRsensorState()) { + handleOffTimer(); + } +} + +void PIRsensorSwitch::addToJsonInfo(JsonObject &root) +{ + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + + String uiDomString; + if (enabled) { + if (offTimerStart > 0) + { + uiDomString = ""; + unsigned int offSeconds = (m_switchOffDelay - (millis() - offTimerStart)) / 1000; + if (offSeconds >= 3600) + { + uiDomString += (offSeconds / 3600); + uiDomString += F("h "); + offSeconds %= 3600; + } + if (offSeconds >= 60) + { + uiDomString += (offSeconds / 60); + offSeconds %= 60; + } + else if (uiDomString.length() > 0) + { + uiDomString += 0; + } + if (uiDomString.length() > 0) + { + uiDomString += F("min "); + } + uiDomString += (offSeconds); + infoArr.add(uiDomString + F("s")); + } else { + infoArr.add(sensorPinState ? F("sensor on") : F("inactive")); + } + } else { + infoArr.add(F("disabled")); + } + + uiDomString = F(" "); + infoArr.add(uiDomString); + + JsonObject sensor = root[F("sensor")]; + if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); + sensor[F("motion")] = sensorPinState || offTimerStart>0 ? true : false; +} + +void PIRsensorSwitch::onStateChange(uint8_t mode) { + if (!initDone) return; + DEBUG_PRINT(F("PIR: offTimerStart=")); DEBUG_PRINTLN(offTimerStart); + if (m_override && PIRtriggered && offTimerStart) { // debounce + // checking PIRtriggered and offTimerStart will prevent cancellation upon On trigger + DEBUG_PRINTLN(F("PIR: Canceled.")); + offTimerStart = 0; + PIRtriggered = false; + } +} + +void PIRsensorSwitch::readFromJsonState(JsonObject &root) +{ + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + if (usermod[FPSTR(_enabled)].is()) { + enabled = usermod[FPSTR(_enabled)].as(); + } + } +} + +void PIRsensorSwitch::addToConfig(JsonObject &root) +{ + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; + top["pin"] = PIRsensorPin; + top[FPSTR(_onPreset)] = m_onPreset; + top[FPSTR(_offPreset)] = m_offPreset; + top[FPSTR(_nightTime)] = m_nightTimeOnly; + top[FPSTR(_mqttOnly)] = m_mqttOnly; + top[FPSTR(_offOnly)] = m_offOnly; + top[FPSTR(_override)] = m_override; + top[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; + top[FPSTR(_notify)] = (NotifyUpdateMode != CALL_MODE_NO_NOTIFY); + DEBUG_PRINTLN(F("PIR config saved.")); +} + +void PIRsensorSwitch::appendConfigData() +{ + oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('PIRsensorSwitch:notifications',1,'Periodic WS updates');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field +} + +bool PIRsensorSwitch::readFromConfig(JsonObject &root) +{ + bool oldEnabled = enabled; + int8_t oldPin = PIRsensorPin; + + DEBUG_PRINT(FPSTR(_name)); + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + PIRsensorPin = top["pin"] | PIRsensorPin; + + enabled = top[FPSTR(_enabled)] | enabled; + + m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; + + m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; + m_onPreset = max(0,min(250,(int)m_onPreset)); + m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; + m_offPreset = max(0,min(250,(int)m_offPreset)); + + m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; + m_mqttOnly = top[FPSTR(_mqttOnly)] | m_mqttOnly; + m_offOnly = top[FPSTR(_offOnly)] | m_offOnly; + m_override = top[FPSTR(_override)] | m_override; + HomeAssistantDiscovery = top[FPSTR(_haDiscovery)] | HomeAssistantDiscovery; + + NotifyUpdateMode = top[FPSTR(_notify)] ? CALL_MODE_DIRECT_CHANGE : CALL_MODE_NO_NOTIFY; + + if (!initDone) { + // reading config prior to setup() + DEBUG_PRINTLN(F(" config loaded.")); + } else { + if (oldPin != PIRsensorPin || oldEnabled != enabled) { + // check if pin is OK + if (oldPin != PIRsensorPin && oldPin >= 0) { + // if we are changing pin in settings page + // deallocate old pin + pinManager.deallocatePin(oldPin, PinOwner::UM_PIR); + if (pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { + pinMode(PIRsensorPin, INPUT_PULLUP); + } else { + // allocation failed + PIRsensorPin = -1; + enabled = false; + } + } + if (enabled) { + sensorPinState = digitalRead(PIRsensorPin); + } + } + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_override)].isNull(); +} diff --git a/usermods/RTC/usermod_rtc.h b/usermods/RTC/usermod_rtc.h index fd9a4054..42965e3a 100644 --- a/usermods/RTC/usermod_rtc.h +++ b/usermods/RTC/usermod_rtc.h @@ -12,8 +12,7 @@ class RTCUsermod : public Usermod { public: void setup() { - PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { disabled = true; return; } + if (i2c_scl<0 || i2c_sda<0) { disabled = true; return; } RTC.begin(); time_t rtcTime = RTC.get(); if (rtcTime) { @@ -25,8 +24,8 @@ class RTCUsermod : public Usermod { } void loop() { - if (strip.isUpdating()) return; - if (!disabled && toki.isTick()) { + if (disabled || strip.isUpdating()) return; + if (toki.isTick()) { time_t t = toki.second(); if (t != RTC.get()) RTC.set(t); //set RTC to NTP/UI-provided value } diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 93700c91..144cccbf 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -17,12 +17,6 @@ #ifndef TFT_HEIGHT #error Please define TFT_HEIGHT #endif - #ifndef TFT_MOSI - #error Please define TFT_MOSI - #endif - #ifndef TFT_SCLK - #error Please define TFT_SCLK - #endif #ifndef TFT_DC #error Please define TFT_DC #endif @@ -140,8 +134,14 @@ class St7789DisplayUsermod : public Usermod { */ void setup() { - PinManagerPinType pins[] = { { TFT_MOSI, true }, { TFT_MISO, false}, { TFT_SCLK, true }, { TFT_CS, true}, { TFT_DC, true}, { TFT_RST, true }, { TFT_BL, true } }; - if (!pinManager.allocateMultiplePins(pins, 7, PinOwner::UM_FourLineDisplay)) { enabled = false; return; } + PinManagerPinType spiPins[] = { { spi_mosi, true }, { spi_miso, false}, { spi_sclk, true } }; + if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } + PinManagerPinType displayPins[] = { { TFT_CS, true}, { TFT_DC, true}, { TFT_RST, true }, { TFT_BL, true } }; + if (!pinManager.allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { + pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + enabled = false; + return; + } tft.init(); tft.setRotation(0); //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip. @@ -365,9 +365,6 @@ class St7789DisplayUsermod : public Usermod { { JsonObject top = root.createNestedObject("ST7789"); JsonArray pins = top.createNestedArray("pin"); - pins.add(TFT_MOSI); - pins.add(TFT_MISO); - pins.add(TFT_SCLK); pins.add(TFT_CS); pins.add(TFT_DC); pins.add(TFT_RST); @@ -376,6 +373,13 @@ class St7789DisplayUsermod : public Usermod { } + void appendConfigData() { + oappend(SET_F("addInfo('ST7789:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('ST7789:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI RST');")); + oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI BL');")); + } + /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) diff --git a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h b/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h index 4a42a7d5..bdf78484 100644 --- a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h +++ b/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h @@ -13,14 +13,6 @@ Adafruit_Si7021 si7021; -#ifdef ARDUINO_ARCH_ESP32 //ESP32 boards -uint8_t SCL_PIN = 22; -uint8_t SDA_PIN = 21; -#else //ESP8266 boards -uint8_t SCL_PIN = 5; -uint8_t SDA_PIN = 4; -#endif - class Si7021_MQTT_HA : public Usermod { private: @@ -184,7 +176,6 @@ class Si7021_MQTT_HA : public Usermod { if (enabled) { Serial.println("Si7021_MQTT_HA: Starting!"); - Wire.begin(SDA_PIN, SCL_PIN); Serial.println("Si7021_MQTT_HA: Initializing sensors.. "); _initializeSensor(); } diff --git a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h index 39c2c3ef..fe6b958f 100644 --- a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h +++ b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h @@ -50,9 +50,7 @@ class UsermodVL53L0XGestures : public Usermod { public: void setup() { - PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { enabled = false; return; } - Wire.begin(); + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } sensor.setTimeout(150); if (!sensor.init()) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index ff5483fd..b0964d4b 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -569,16 +569,6 @@ class AudioReactive : public Usermod { #else int8_t i2sckPin = I2S_CKPIN; #endif - #ifndef ES7243_SDAPIN - int8_t sdaPin = -1; - #else - int8_t sdaPin = ES7243_SDAPIN; - #endif - #ifndef ES7243_SCLPIN - int8_t sclPin = -1; - #else - int8_t sclPin = ES7243_SCLPIN; - #endif #ifndef MCLK_PIN int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ #else @@ -1136,7 +1126,7 @@ class AudioReactive : public Usermod { DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); delay(100); - if (audioSource) audioSource->initialize(sdaPin, sclPin, i2swsPin, i2ssdPin, i2sckPin, mclkPin); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; case 3: DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); @@ -1664,8 +1654,6 @@ class AudioReactive : public Usermod { pinArray.add(i2swsPin); pinArray.add(i2sckPin); pinArray.add(mclkPin); - pinArray.add(sdaPin); - pinArray.add(sclPin); JsonObject cfg = top.createNestedObject("config"); cfg[F("squelch")] = soundSquelch; @@ -1726,8 +1714,6 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][3], mclkPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][4], sdaPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][5], sclPin); configComplete &= getJsonValue(top["config"][F("squelch")], soundSquelch); configComplete &= getJsonValue(top["config"][F("gain")], sampleGain); @@ -1793,8 +1779,6 @@ class AudioReactive : public Usermod { #else oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); #endif - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',4,'','I2C SDA');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',5,'','I2C SCL');")); } diff --git a/usermods/audioreactive/audio_source.h b/usermods/audioreactive/audio_source.h index fdafee1d..c230dcc0 100644 --- a/usermods/audioreactive/audio_source.h +++ b/usermods/audioreactive/audio_source.h @@ -1,6 +1,5 @@ #pragma once -#include #include "wled.h" #include #include @@ -383,21 +382,12 @@ class I2SSource : public AudioSource { */ class ES7243 : public I2SSource { private: - // I2C initialization functions for ES7243 - void _es7243I2cBegin() { - bool i2c_initialized = Wire.begin(pin_ES7243_SDA, pin_ES7243_SCL, 100000U); - if (i2c_initialized == false) { - DEBUGSR_PRINTLN(F("AR: ES7243 failed to initialize I2C bus driver.")); - } - } void _es7243I2cWrite(uint8_t reg, uint8_t val) { -#ifndef ES7243_ADDR - Wire.beginTransmission(0x13); - #define ES7243_ADDR 0x13 // default address -#else + #ifndef ES7243_ADDR + #define ES7243_ADDR 0x13 // default address + #endif Wire.beginTransmission(ES7243_ADDR); -#endif Wire.write((uint8_t)reg); Wire.write((uint8_t)val); uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK @@ -407,7 +397,6 @@ class ES7243 : public I2SSource { } void _es7243InitAdc() { - _es7243I2cBegin(); _es7243I2cWrite(0x00, 0x01); _es7243I2cWrite(0x06, 0x00); _es7243I2cWrite(0x05, 0x1B); @@ -422,44 +411,20 @@ public: _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; }; - void initialize(int8_t sdaPin, int8_t sclPin, int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - // check that pins are valid - if ((sdaPin < 0) || (sclPin < 0)) { - DEBUGSR_PRINTF("\nAR: invalid ES7243 I2C pins: SDA=%d, SCL=%d\n", sdaPin, sclPin); - return; - } - + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { if ((i2sckPin < 0) || (mclkPin < 0)) { DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); return; } - // Reserve SDA and SCL pins of the I2C interface - PinManagerPinType es7243Pins[2] = { { sdaPin, true }, { sclPin, true } }; - if (!pinManager.allocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C)) { - pinManager.deallocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C); - DEBUGSR_PRINTF("\nAR: Failed to allocate ES7243 I2C pins: SDA=%d, SCL=%d\n", sdaPin, sclPin); - return; - } - - pin_ES7243_SDA = sdaPin; - pin_ES7243_SCL = sclPin; - // First route mclk, then configure ADC over I2C, then configure I2S _es7243InitAdc(); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); } void deinitialize() { - // Release SDA and SCL pins of the I2C interface - PinManagerPinType es7243Pins[2] = { { pin_ES7243_SDA, true }, { pin_ES7243_SCL, true } }; - pinManager.deallocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C); I2SSource::deinitialize(); } - - private: - int8_t pin_ES7243_SDA; - int8_t pin_ES7243_SCL; }; /* ES8388 Sound Modude diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/usermod_mpu6050_imu.h index b4dc01a4..748ddf1a 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/usermod_mpu6050_imu.h @@ -85,12 +85,9 @@ class MPU6050Driver : public Usermod { * setup() is called once at boot. WiFi is not yet connected at this point. */ void setup() { - PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { enabled = false; return; } - // join I2C bus (I2Cdev library doesn't do this automatically) + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE - Wire.begin(); - Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties + Wire.setClock(400000U); // 400kHz I2C clock. Comment this line if having compilation difficulties #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE Fastwire::setup(400, true); #endif 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 de68e11b..7234df90 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -4,10 +4,19 @@ #ifndef MULTI_RELAY_MAX_RELAYS #define MULTI_RELAY_MAX_RELAYS 4 +#else + #if MULTI_RELAY_MAX_RELAYS>8 + #undef MULTI_RELAY_MAX_RELAYS + #define MULTI_RELAY_MAX_RELAYS 8 + #warning Maximum relays set to 8 + #endif #endif #ifndef MULTI_RELAY_PINS #define MULTI_RELAY_PINS -1 + #define MULTI_RELAY_ENABLED false +#else + #define MULTI_RELAY_ENABLED true #endif #define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) @@ -15,21 +24,37 @@ #define ON true #define OFF false +#ifndef USERMOD_USE_PCF8574 + #undef USE_PCF8574 + #define USE_PCF8574 false +#else + #undef USE_PCF8574 + #define USE_PCF8574 true +#endif + +#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 invert : 1; // does On mean 1 or 0 + 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; @@ -37,22 +62,17 @@ class MultiRelay : public Usermod { private: // array of relays - Relay _relay[MULTI_RELAY_MAX_RELAYS]; + Relay _relay[MULTI_RELAY_MAX_RELAYS]; - // switch timer start time - uint32_t _switchTimerStart = 0; - // old brightness - bool _oldMode; - - // usermod enabled - bool enabled = false; // needs to be configured (no default config) - // status of initialisation - bool initDone = false; - - bool HAautodiscovery = false; - - uint16_t periodicBroadcastSec = 60; - unsigned long lastBroadcast = 0; + uint32_t _switchTimerStart; // switch timer start time + bool _oldMode; // old brightness + bool enabled; // usermod enabled + bool initDone; // status of initialisation + bool usePcf8574; + uint8_t addrPcf8574; + bool HAautodiscovery; + uint16_t periodicBroadcastSec; + unsigned long lastBroadcast; // strings to reduce flash memory usage (used more than twice) static const char _name[]; @@ -64,160 +84,53 @@ 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,354 +139,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(address); + Wire.write(_data); + return Wire.endTransmission(); +} + +//Read a byte from the IO expander +byte MultiRelay::IOexpanderRead(int address) { + byte _data = 0; + Wire.requestFrom(address, 1); + if (Wire.available()) { + _data = Wire.read(); + } + return _data; +} + + +// public methods + +MultiRelay::MultiRelay() + : _switchTimerStart(0) + , enabled(MULTI_RELAY_ENABLED) + , initDone(false) + , usePcf8574(USE_PCF8574) + , addrPcf8574(PCF8574_ADDRESS) + , HAautodiscovery(false) + , periodicBroadcastSec(60) + , lastBroadcast(0) +{ + 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; + if (usePcf8574 && _relay[relay].pin >= 100) { + // we need to send all ouputs at the same time + uint8_t state = 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 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<0 || i2c_scl<0) usePcf8574 = false; + + uint8_t state = 0; + for (int i=0; i= 100) { + uint8_t pin = _relay[i].pin - 100; + if (!_relay[i].external) _relay[i].state = !offMode; + state |= (uint8_t)(_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin; + } else if (_relay[i].pin<100 && _relay[i].pin>=0) { + if (pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { + if (!_relay[i].external) _relay[i].state = !offMode; + switchRelay(i, _relay[i].state); + _relay[i].active = false; + } else { + _relay[i].pin = -1; // allocation failed + } + } + } + if (usePcf8574) { + IOexpanderWrite(addrPcf8574, state); // init expander (set all outputs) + DEBUG_PRINTLN(F("PCF8574(s) inited.")); + } + _oldMode = offMode; + initDone = true; +} + +/** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ +void MultiRelay::loop() { + yield(); + if (!enabled || strip.isUpdating()) return; + + static unsigned long lastUpdate = 0; + if (millis() - lastUpdate < 100) return; // update only 10 times/s + lastUpdate = millis(); + + //set relay when LEDs turn on + if (_oldMode != offMode) { + _oldMode = offMode; + _switchTimerStart = millis(); + for (int i=0; i=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 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!)');")); + oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); + //oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); + oappend(SET_F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); +} + +/** + * 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<0 || i2c_scl<0) 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 && oldPin[i]<100) { + pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); + } + // allocate new pins + setup(); + 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 = "PCF8574-address"; diff --git a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h b/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h index 972e2c86..4f51750a 100644 --- a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h +++ b/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h @@ -6,7 +6,6 @@ #include "wled.h" #include -#include #include #include #include @@ -16,14 +15,6 @@ Adafruit_BMP280 bmp; Adafruit_Si7021 si7021; Adafruit_CCS811 ccs811; -#ifdef ARDUINO_ARCH_ESP32 //ESP32 boards -uint8_t SCL_PIN = 22; -uint8_t SDA_PIN = 21; -#else //ESP8266 boards -uint8_t SCL_PIN = 5; -uint8_t SDA_PIN = 4; -#endif - class UserMod_SensorsToMQTT : public Usermod { private: @@ -231,7 +222,6 @@ public: void setup() { Serial.println("Starting!"); - Wire.begin(SDA_PIN, SCL_PIN); Serial.println("Initializing sensors.. "); _initialize(); } diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/usermod_sht.h index 56f3dd41..56cea219 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/usermod_sht.h @@ -16,7 +16,6 @@ class ShtUsermod : public Usermod private: bool enabled = false; // Is usermod enabled or not bool firstRunDone = false; // Remembers if the first config load run had been done - bool pinAllocDone = true; // Remembers if we have allocated pins bool initDone = false; // Remembers if the mod has been completely initialised bool haMqttDiscovery = false; // Is MQTT discovery enabled or not bool haMqttDiscoveryDone = false; // Remembers if we already published the HA discovery topics @@ -94,7 +93,7 @@ void ShtUsermod::initShtTempHumiditySensor() case USERMOD_SHT_TYPE_SHT85: shtTempHumidSensor = (SHT *) new SHT85(); break; } - shtTempHumidSensor->begin(shtI2cAddress, i2c_sda, i2c_scl); + shtTempHumidSensor->begin(shtI2cAddress); // uses &Wire if (shtTempHumidSensor->readStatus() == 0xFFFF) { DEBUG_PRINTF("[%s] SHT init failed!\n", _name); cleanup(); @@ -132,13 +131,6 @@ void ShtUsermod::cleanupShtTempHumiditySensor() void ShtUsermod::cleanup() { cleanupShtTempHumiditySensor(); - - if (pinAllocDone) { - PinManagerPinType pins[2] = { { i2c_sda, true }, { i2c_scl, true } }; - pinManager.deallocateMultiplePins(pins, 2, PinOwner::HW_I2C); - pinAllocDone = false; - } - enabled = false; } @@ -237,14 +229,12 @@ void ShtUsermod::appendDeviceToMqttDiscoveryMessage(JsonDocument& root) { void ShtUsermod::setup() { if (enabled) { - PinManagerPinType pins[2] = { { i2c_sda, true }, { i2c_scl, true } }; - // GPIOs can be set to -1 and allocateMultiplePins() will return true, so check they're gt zero - if (i2c_sda < 0 || i2c_scl < 0 || !pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { - DEBUG_PRINTF("[%s] SHT pin allocation failed!\n", _name); + // GPIOs can be set to -1 , so check they're gt zero + if (i2c_sda < 0 || i2c_scl < 0) { + DEBUG_PRINTF("[%s] I2C bus not initialised!\n", _name); cleanup(); return; } - pinAllocDone = true; initShtTempHumiditySensor(); diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index ab244ef2..5a99c3cd 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -1,20 +1,24 @@ #pragma once #include "wled.h" +#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible #include // from https://github.com/olikraus/u8g2/ #include "4LD_wled_fonts.c" +#ifndef FLD_ESP32_NO_THREADS + #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! +#endif + // -// Insired by the usermod_v2_four_line_display +// Inspired by the usermod_v2_four_line_display // // v2 usermod for using 128x32 or 128x64 i2c // OLED displays to provide a four line display // for WLED. // // Dependencies -// * This usermod REQURES the ModeSortUsermod // * This Usermod works best, by far, when coupled -// with RotaryEncoderUIUsermod. +// with RotaryEncoderUI ALT Usermod. // // Make sure to enable NTP and set your time zone in WLED Config | Time. // @@ -23,22 +27,14 @@ // REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) // REQUIREMENT: * Wire // +// If display does not work or looks corrupted check the +// constructor reference: +// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp +// or check the gallery: +// https://github.com/olikraus/u8g2/wiki/gallery -//The SCL and SDA pins are defined here. -#ifndef FLD_PIN_SCL - #define FLD_PIN_SCL i2c_scl -#endif -#ifndef FLD_PIN_SDA - #define FLD_PIN_SDA i2c_sda -#endif -#ifndef FLD_PIN_CLOCKSPI - #define FLD_PIN_CLOCKSPI spi_sclk -#endif - #ifndef FLD_PIN_DATASPI - #define FLD_PIN_DATASPI spi_mosi -#endif #ifndef FLD_PIN_CS - #define FLD_PIN_CS spi_cs + #define FLD_PIN_CS 15 #endif #ifdef ARDUINO_ARCH_ESP32 @@ -86,20 +82,23 @@ void DisplayTaskCode(void * parameter); typedef enum { NONE = 0, - SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C - SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C - SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C - SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C - SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C - SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI - SSD1306_SPI64 // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C + SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C + SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C + SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C + SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C + SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI + SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1309_SPI64 // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI } DisplayType; class FourLineDisplayUsermod : public Usermod { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) public: FourLineDisplayUsermod() { if (!instance) instance = this; } static FourLineDisplayUsermod* getInstance(void) { return instance; } +#endif private: @@ -112,10 +111,10 @@ class FourLineDisplayUsermod : public Usermod { U8X8 *u8x8 = nullptr; // pointer to U8X8 display object #ifndef FLD_SPI_DEFAULT - int8_t ioPin[5] = {FLD_PIN_SCL, FLD_PIN_SDA, -1, -1, -1}; // I2C pins: SCL, SDA + int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) #else - int8_t ioPin[5] = {FLD_PIN_CLOCKSPI, FLD_PIN_DATASPI, FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // SPI pins: CLK, MOSI, CS, DC, RST + int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) #endif @@ -178,561 +177,78 @@ class FourLineDisplayUsermod : public Usermod { // https://github.com/olikraus/u8g2/wiki/gallery // some displays need this to properly apply contrast - void setVcomh(bool highContrast) { - u8x8_t *u8x8_struct = u8x8->getU8x8(); - u8x8_cad_StartTransfer(u8x8_struct); - u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value - u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64 - u8x8_cad_EndTransfer(u8x8_struct); - } + void setVcomh(bool highContrast); + void startDisplay(); /** * Wrappers for screen drawing */ - void setFlipMode(uint8_t mode) { - if (type == NONE || !enabled) return; - u8x8->setFlipMode(mode); - } - void setContrast(uint8_t contrast) { - if (type == NONE || !enabled) return; - u8x8->setContrast(contrast); - } - void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(u8x8_font_chroma48medium8_r); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); - else u8x8->drawString(col, row, string); - drawing = false; - } - void draw2x2String(uint8_t col, uint8_t row, const char *string) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(u8x8_font_chroma48medium8_r); - u8x8->draw2x2String(col, row, string); - drawing = false; - } - void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(font); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); - else u8x8->drawGlyph(col, row, glyph); - drawing = false; - } - void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(font); - u8x8->draw2x2Glyph(col, row, glyph); - drawing = false; - } - uint8_t getCols() { - if (type==NONE || !enabled) return 0; - return u8x8->getCols(); - } - void clear() { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->clear(); - drawing = false; - } - void setPowerSave(uint8_t save) { - if (type == NONE || !enabled) return; - u8x8->setPowerSave(save); - } - - void center(String &line, uint8_t width) { - int len = line.length(); - if (len0; i--) line = ' ' + line; - for (byte i=line.length(); i 11) { AmPmHour -= 12; isitAM = false; } - if (AmPmHour == 0) { AmPmHour = 12; } - } - if (knownHour != hourCurrent) { - // only update date when hour changes - sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); - draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day - } - sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); - draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds - if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time - - drawStatusIcons(); //icons power, wifi, timer, etc - - knownMinute = minuteCurrent; - knownHour = hourCurrent; - } - if (showSeconds && secondCurrent != lastSecond) { - lastSecond = secondCurrent; - draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":"); - sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); - drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line - } - } + void showTime(); /** * Enable sleep (turn the display off) or clock mode. */ - void sleepOrClock(bool enabled) { - if (enabled) { - displayTurnedOff = true; - if (clockMode && ntpEnabled) { - knownMinute = knownHour = 99; - showTime(); - } else - setPowerSave(1); - } else { - displayTurnedOff = false; - setPowerSave(0); - } - } + void sleepOrClock(bool enabled); public: // gets called once at boot. Do all initialization that doesn't depend on // network here - void setup() { - if (type == NONE || !enabled) return; - - bool isHW, isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64); - PinOwner po = PinOwner::UM_FourLineDisplay; - if (isSPI) { - uint8_t hw_sclk = spi_sclk<0 ? HW_PIN_CLOCKSPI : spi_sclk; - uint8_t hw_mosi = spi_mosi<0 ? HW_PIN_DATASPI : spi_mosi; - if (ioPin[0] < 0 || ioPin[1] < 0) { - ioPin[0] = hw_sclk; - ioPin[1] = hw_mosi; - } - isHW = (ioPin[0]==hw_sclk && ioPin[1]==hw_mosi); - PinManagerPinType cspins[3] = { { ioPin[2], true }, { ioPin[3], true }, { ioPin[4], true } }; - if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type=NONE; return; } - if (isHW) po = PinOwner::HW_SPI; // allow multiple allocations of HW I2C bus pins - PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; - if (!pinManager.allocateMultiplePins(pins, 2, po)) { - pinManager.deallocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay); - type = NONE; - return; - } - } else { - uint8_t hw_scl = i2c_scl<0 ? HW_PIN_SCL : i2c_scl; - uint8_t hw_sda = i2c_sda<0 ? HW_PIN_SDA : i2c_sda; - if (ioPin[0] < 0 || ioPin[1] < 0) { - ioPin[0] = hw_scl; - ioPin[1] = hw_sda; - } - isHW = (ioPin[0]==hw_scl && ioPin[1]==hw_sda); - if (isHW) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - PinManagerPinType pins[2] = { {ioPin[0], true }, { ioPin[1], true } }; - if (!pinManager.allocateMultiplePins(pins, 2, po)) { type=NONE; return; } - } - - DEBUG_PRINTLN(F("Allocating display.")); -/* -// At some point it may be good to not new/delete U8X8 object but use this instead -// (does not currently work) -//------------------------------------------------------------------------------- - switch (type) { - case SSD1306: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SH1106: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1305: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1305_64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_SPI: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_SPI64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); - break; - default: - type = NONE; - return; - } - if (isSPI) { - if (!isHW) u8x8_SetPin_4Wire_SW_SPI(u8x8.getU8x8(), ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8_SetPin_4Wire_HW_SPI(u8x8.getU8x8(), ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - } else { - if (!isHW) u8x8_SetPin_SW_I2C(u8x8.getU8x8(), ioPin[0], ioPin[1], U8X8_PIN_NONE); // SCL, SDA, reset - else u8x8_SetPin_HW_I2C(u8x8.getU8x8(), U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - } -*/ - switch (type) { - case SSD1306: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SH1106: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1306_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1305: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1305_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1306_SPI: - // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - break; - case SSD1306_SPI64: - // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - break; - default: - u8x8 = nullptr; - } - - if (nullptr == u8x8) { - DEBUG_PRINTLN(F("Display init failed.")); - pinManager.deallocateMultiplePins((const uint8_t*)ioPin, isSPI ? 5 : 2, po); - type = NONE; - return; - } - - lineHeight = u8x8->getRows() > 4 ? 2 : 1; - DEBUG_PRINTLN(F("Starting display.")); - u8x8->setBusClock(ioFrequency); // can be used for SPI too - u8x8->begin(); - setFlipMode(flip); - setVcomh(contrastFix); - setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 - setPowerSave(0); - //drawString(0, 0, "Loading..."); - overlayLogo(3500); - onUpdateBegin(false); // create Display task - initDone = true; - } + void setup(); // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here - void connected() { - knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : - knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); - networkOverlay(PSTR("NETWORK INFO"),7000); - } + void connected(); /** * Da loop. */ - void loop() { - #ifndef ARDUINO_ARCH_ESP32 - if (!enabled || strip.isUpdating()) return; - unsigned long now = millis(); - if (now < nextUpdate) return; - nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate); - redraw(false); - #endif - } + void loop(); //function to update lastredraw - void updateRedrawTime() { - lastRedraw = millis(); - } + inline void updateRedrawTime() { lastRedraw = millis(); } /** * Redraw the screen (but only if things have changed * or if forceRedraw). */ - void redraw(bool forceRedraw) { - bool needRedraw = false; - unsigned long now = millis(); - - if (type == NONE || !enabled) return; - if (overlayUntil > 0) { - if (now >= overlayUntil) { - // Time to display the overlay has elapsed. - overlayUntil = 0; - forceRedraw = true; - } else { - // We are still displaying the overlay - // Don't redraw. - return; - } - } + void redraw(bool forceRedraw); - while (drawing && millis()-now < 25) delay(1); // wait if someone else is drawing - if (drawing || lockRedraw) return; - - if (apActive && WLED_WIFI_CONFIGURED && now<15000) { - knownSsid = apSSID; - networkOverlay(PSTR("NETWORK INFO"),30000); - return; - } - - // Check if values which are shown on display changed from the last time. - if (forceRedraw) { - needRedraw = true; - clear(); - } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon - powerON = !powerON; - drawStatusIcons(); - return; - } else if (knownnightlight != nightlightActive) { //trigger moon icon - knownnightlight = nightlightActive; - drawStatusIcons(); - if (knownnightlight) { - String timer = PSTR("Timer On"); - center(timer,LINE_BUFFER_SIZE-1); - overlay(timer.c_str(), 2500, 6); - } - return; - } else if (wificonnected != interfacesInited) { //trigger wifi icon - wificonnected = interfacesInited; - drawStatusIcons(); - return; - } else if (knownMode != effectCurrent || knownPalette != effectPalette) { - if (displayTurnedOff) needRedraw = true; - else { - if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } - if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } - lastRedraw = now; - return; - } - } else if (knownBrightness != bri) { - if (displayTurnedOff && nightlightActive) { knownBrightness = bri; } - else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; } - } else if (knownEffectSpeed != effectSpeed) { - if (displayTurnedOff) needRedraw = true; - else { updateSpeed(); lastRedraw = now; return; } - } else if (knownEffectIntensity != effectIntensity) { - if (displayTurnedOff) needRedraw = true; - else { updateIntensity(); lastRedraw = now; return; } - } - - if (!needRedraw) { - // Nothing to change. - // Turn off display after 1 minutes with no change. - if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { - // We will still check if there is a change in redraw() - // and turn it back on if it changed. - clear(); - sleepOrClock(true); - } else if (displayTurnedOff && ntpEnabled) { - showTime(); - } - return; - } - - lastRedraw = now; - - // Turn the display back on - wakeDisplay(); - - // Update last known values. - knownBrightness = bri; - knownMode = effectCurrent; - knownPalette = effectPalette; - knownEffectSpeed = effectSpeed; - knownEffectIntensity = effectIntensity; - knownnightlight = nightlightActive; - wificonnected = interfacesInited; - - // Do the actual drawing - // First row: Icons - draw2x2GlyphIcons(); - drawArrow(); - drawStatusIcons(); - - // Second row - updateBrightness(); - updateSpeed(); - updateIntensity(); - - // Third row - showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info - - // Fourth row - showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info - } - - void updateBrightness() { - knownBrightness = bri; - if (overlayUntil == 0) { - lockRedraw = true; - brightness100 = ((uint16_t)bri*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), brightness100); - drawString(1, lineHeight, lineBuffer); - lockRedraw = false; - } - } - - void updateSpeed() { - knownEffectSpeed = effectSpeed; - if (overlayUntil == 0) { - lockRedraw = true; - fxspeed100 = ((uint16_t)effectSpeed*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), fxspeed100); - drawString(5, lineHeight, lineBuffer); - lockRedraw = false; - } - } - - void updateIntensity() { - knownEffectIntensity = effectIntensity; - if (overlayUntil == 0) { - lockRedraw = true; - fxintensity100 = ((uint16_t)effectIntensity*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), fxintensity100); - drawString(9, lineHeight, lineBuffer); - lockRedraw = false; - } - } - - void drawStatusIcons() { - uint8_t col = 15; - uint8_t row = 0; - lockRedraw = true; - drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon - if (lineHeight==2) { col--; } else { row++; } - drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon - if (lineHeight==2) { col--; } else { col = row = 0; } - drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode - lockRedraw = false; - } + void updateBrightness(); + void updateSpeed(); + void updateIntensity(); + void drawStatusIcons(); /** * marks the position of the arrow showing * the current setting being changed * pass line and colum info */ - void setMarkLine(byte newMarkLineNum, byte newMarkColNum) { - markLineNum = newMarkLineNum; - markColNum = newMarkColNum; - } + void setMarkLine(byte newMarkLineNum, byte newMarkColNum); //Draw the arrow for the current setting beiong changed - void drawArrow() { - lockRedraw = true; - if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1); - lockRedraw = false; - } + void drawArrow(); //Display the current effect or palette (desiredEntry) // on the appropriate line (row). - void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { - char lineBuffer[MAX_JSON_CHARS]; - if (overlayUntil == 0) { - lockRedraw = true; - // Find the mode name in JSON - uint8_t printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1); - if (lineBuffer[0]=='*' && lineBuffer[1]==' ') { - // remove "* " from dynamic palettes - for (byte i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\0' - printedChars -= 2; - } else if ((lineBuffer[0]==' ' && lineBuffer[1]>127)) { - // remove note symbol from effect names - for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' - printedChars -= 5; - } - if (lineHeight == 2) { // use this code for 8 line display - char smallBuffer1[MAX_MODE_LINE_SPACE]; - char smallBuffer2[MAX_MODE_LINE_SPACE]; - uint8_t smallChars1 = 0; - uint8_t smallChars2 = 0; - if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits - while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; - lineBuffer[printedChars] = 0; - drawString(1, row*lineHeight, lineBuffer); - } else { // for long names divide the text into 2 lines and print them small - bool spaceHit = false; - for (uint8_t i = 0; i < printedChars; i++) { - switch (lineBuffer[i]) { - case ' ': - if (i > 4 && !spaceHit) { - spaceHit = true; - break; - } - if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; - else smallBuffer1[smallChars1++] = lineBuffer[i]; - break; - default: - if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; - else smallBuffer1[smallChars1++] = lineBuffer[i]; - break; - } - } - while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; - smallBuffer1[smallChars1] = 0; - drawString(1, row*lineHeight, smallBuffer1, true); - while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; - smallBuffer2[smallChars2] = 0; - drawString(1, row*lineHeight+1, smallBuffer2, true); - } - } else { // use this code for 4 ling displays - char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette - uint8_t smallChars3 = 0; - for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; - smallBuffer3[smallChars3] = 0; - drawString(1, row*lineHeight, smallBuffer3, true); - } - lockRedraw = false; - } - } + void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); /** * If there screen is off or in clock is displayed, @@ -740,330 +256,59 @@ class FourLineDisplayUsermod : public Usermod { * the first input from the rotary encoder but * to wake up the screen. */ - bool wakeDisplay() { - if (type == NONE || !enabled) return false; - if (displayTurnedOff) { - unsigned long now = millis(); - while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing - if (drawing) return false; - lockRedraw = true; - clear(); - // Turn the display back on - sleepOrClock(false); - lockRedraw = false; - return true; - } - return false; - } + bool wakeDisplay(); /** * Allows you to show one line and a glyph as overlay for a period of time. * Clears the screen and prints. * Used in Rotary Encoder usermod. */ - void overlay(const char* line1, long showHowLong, byte glyphType) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (glyphType>0 && glyphType<255) { - if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font - else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true); - } - if (line1) { - String buf = line1; - center(buf, getCols()); - drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str()); - } - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } + void overlay(const char* line1, long showHowLong, byte glyphType); /** * Allows you to show Akemi WLED logo overlay for a period of time. * Clears the screen and prints. */ - void overlayLogo(long showHowLong) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (lineHeight == 2) { - //add a bit of randomness - switch (millis()%3) { - case 0: - //WLED - draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2); - draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2); - draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2); - draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2); - break; - case 1: - //WLED Akemi - drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true); - drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true); - drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true); - break; - case 2: - //Akemi - //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font - drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true); - drawString(6, 6, "WLED"); - break; - } - } else { - switch (millis()%3) { - case 0: - //WLED - draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2); - draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2); - draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2); - draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2); - break; - case 1: - //WLED Akemi - drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4); - drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4); - drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4); - break; - case 2: - //Akemi - //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash - draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2); - break; - } - } - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } + void overlayLogo(long showHowLong); /** * Allows you to show two lines as overlay for a period of time. * Clears the screen and prints. * Used in Auto Save usermod */ - void overlay(const char* line1, const char* line2, long showHowLong) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (line1) { - String buf = line1; - center(buf, getCols()); - drawString(0, 1*lineHeight, buf.c_str()); - } - if (line2) { - String buf = line2; - center(buf, getCols()); - drawString(0, 2*lineHeight, buf.c_str()); - } - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } - - void networkOverlay(const char* line1, long showHowLong) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - - String line; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (line1) { - line = line1; - center(line, getCols()); - drawString(0, 0, line.c_str()); - } - // Second row with Wifi name - line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); - if (line.length() < getCols()) center(line, getCols()); - drawString(0, lineHeight, line.c_str()); - // Print `~` char to indicate that SSID is longer, than our display - if (knownSsid.length() > getCols()) { - drawString(getCols() - 1, 0, "~"); - } - // Third row with IP and Password in AP Mode - line = knownIp.toString(); - center(line, getCols()); - drawString(0, lineHeight*2, line.c_str()); - line = ""; - if (apActive) { - line = apPass; - } else if (strcmp(serverDescription, "WLED") != 0) { - line = serverDescription; - } - center(line, getCols()); - drawString(0, lineHeight*3, line.c_str()); - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } + void overlay(const char* line1, const char* line2, long showHowLong); + void networkOverlay(const char* line1, long showHowLong); /** * 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 - || b // butto 0 only - || buttonType[b] == BTN_TYPE_SWITCH - || 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); - unsigned long now = millis(); - static bool buttonPressedBefore = false; - static bool buttonLongPressed = false; - static unsigned long buttonPressedTime = 0; - static unsigned long buttonWaitTime = 0; - bool handled = true; - - //momentary button logic - if (isButtonPressed(b)) { //pressed - - if (!buttonPressedBefore) buttonPressedTime = now; - buttonPressedBefore = true; - - if (now - buttonPressedTime > 600) { //long press - buttonLongPressed = true; - //TODO: handleButton() handles button 0 without preset in a different way for double click - //so we need to override with same behaviour - longPressAction(0); - //handled = false; - } - - } else if (!isButtonPressed(b) && buttonPressedBefore) { //released - - long dur = now - buttonPressedTime; - if (dur < 50) { - buttonPressedBefore = false; - return true; - } //too short "press", debounce - - bool doublePress = buttonWaitTime; //did we have short press before? - buttonWaitTime = 0; - - if (!buttonLongPressed) { //short press - // if this is second release within 350ms it is a double press (buttonWaitTime!=0) - //TODO: handleButton() handles button 0 without preset in a different way for double click - if (doublePress) { - networkOverlay(PSTR("NETWORK INFO"),7000); - handled = true; - } else { - buttonWaitTime = now; - } - } - buttonPressedBefore = false; - buttonLongPressed = false; - } - // if 350ms elapsed since last press/release it is a short press - if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) { - buttonWaitTime = 0; - //TODO: handleButton() handles button 0 without preset in a different way for double click - //so we need to override with same behaviour - shortPressAction(0); - //handled = false; - } - return handled; - } - - #if CONFIG_FREERTOS_UNICORE - #define ARDUINO_RUNNING_CORE 0 - #else - #define ARDUINO_RUNNING_CORE 1 - #endif - void onUpdateBegin(bool init) { - #ifdef ARDUINO_ARCH_ESP32 - if (init && Display_Task) { - vTaskSuspend(Display_Task); // update is about to begin, disable task to prevent crash - } else { - // update has failed or create task requested - if (Display_Task) - vTaskResume(Display_Task); - else - xTaskCreatePinnedToCore( - [](void * par) { // Function to implement the task - // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; - TickType_t xLastWakeTime = xTaskGetTickCount(); - for(;;) { - delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. - // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. - vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, by doing nothing for REFRESH_RATE_MS millis - FourLineDisplayUsermod::getInstance()->redraw(false); - } - }, - "4LD", // Name of the task - 3072, // Stack size in words - NULL, // Task input parameter - 1, // Priority of the task (not idle) - &Display_Task, // Task handle - ARDUINO_RUNNING_CORE - ); - } - #endif - } + void onUpdateBegin(bool init); /* * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor */ - //void addToJsonInfo(JsonObject& root) { - //JsonObject user = root["u"]; - //if (user.isNull()) user = root.createNestedObject("u"); - //JsonArray data = user.createNestedArray(F("4LineDisplay")); - //data.add(F("Loaded.")); - //} + //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) { - //} + //void addToJsonState(JsonObject& root); /* * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - //void readFromJsonState(JsonObject& root) { - // if (!initDone) return; // prevent crash on boot applyPreset() - //} + //void readFromJsonState(JsonObject& root); - void appendConfigData() { - oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'SSD1306',1);")); - oappend(SET_F("addOption(dd,'SH1106',2);")); - oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); - oappend(SET_F("addOption(dd,'SSD1305',4);")); - oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'-1 use global','I2C/SPI CLK');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'-1 use global','I2C/SPI DTA');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI CS');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',3,'','SPI DC');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',4,'','SPI RST');")); - } + void appendConfigData(); /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -1079,40 +324,7 @@ class FourLineDisplayUsermod : public Usermod { * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ - void addToConfig(JsonObject& root) { - // determine if we are using global HW pins (data & clock) - int8_t hw_dta, hw_clk; - if ((type == SSD1306_SPI || type == SSD1306_SPI64)) { - hw_clk = spi_sclk<0 ? HW_PIN_CLOCKSPI : spi_sclk; - hw_dta = spi_mosi<0 ? HW_PIN_DATASPI : spi_mosi; - } else { - hw_clk = i2c_scl<0 ? HW_PIN_SCL : i2c_scl; - hw_dta = i2c_sda<0 ? HW_PIN_SDA : i2c_sda; - } - - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - - JsonArray io_pin = top.createNestedArray("pin"); - for (int i=0; i<5; i++) { - if (i==0 && ioPin[i]==hw_clk) io_pin.add(-1); // do not store global HW pin - else if (i==1 && ioPin[i]==hw_dta) io_pin.add(-1); // do not store global HW pin - else io_pin.add(ioPin[i]); - } - top["type"] = type; - top[FPSTR(_flip)] = (bool) flip; - top[FPSTR(_contrast)] = contrast; - top[FPSTR(_contrastFix)] = (bool) contrastFix; - #ifndef ARDUINO_ARCH_ESP32 - top[FPSTR(_refreshRate)] = refreshRate; - #endif - top[FPSTR(_screenTimeOut)] = screenTimeout/1000; - top[FPSTR(_sleepMode)] = (bool) sleepMode; - top[FPSTR(_clockMode)] = (bool) clockMode; - top[FPSTR(_showSeconds)] = (bool) showSeconds; - top[FPSTR(_busClkFrequency)] = ioFrequency/1000; - DEBUG_PRINTLN(F("4 Line Display config saved.")); - } + void addToConfig(JsonObject& root); /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). @@ -1122,80 +334,7 @@ class FourLineDisplayUsermod : public Usermod { * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) */ - bool readFromConfig(JsonObject& root) { - bool needsRedraw = false; - DisplayType newType = type; - int8_t oldPin[5]; for (byte i=0; i<5; i++) oldPin[i] = ioPin[i]; - - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - enabled = top[FPSTR(_enabled)] | enabled; - newType = top["type"] | newType; - for (byte i=0; i<5; i++) ioPin[i] = top["pin"][i] | ioPin[i]; - flip = top[FPSTR(_flip)] | flip; - contrast = top[FPSTR(_contrast)] | contrast; - #ifndef ARDUINO_ARCH_ESP32 - refreshRate = top[FPSTR(_refreshRate)] | refreshRate; - refreshRate = min(5000, max(250, (int)refreshRate)); - #endif - screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; - sleepMode = top[FPSTR(_sleepMode)] | sleepMode; - clockMode = top[FPSTR(_clockMode)] | clockMode; - showSeconds = top[FPSTR(_showSeconds)] | showSeconds; - contrastFix = top[FPSTR(_contrastFix)] | contrastFix; - if (newType == SSD1306_SPI || newType == SSD1306_SPI64) - ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - else - ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json - type = newType; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - bool pinsChanged = false; - for (byte i=0; i<5; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } - if (pinsChanged || type!=newType) { - if (type != NONE) delete u8x8; - PinOwner po = PinOwner::UM_FourLineDisplay; - bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64); - if (isSPI) { - pinManager.deallocateMultiplePins((const uint8_t *)(&oldPin[2]), 3, po); - uint8_t hw_sclk = spi_sclk<0 ? HW_PIN_CLOCKSPI : spi_sclk; - uint8_t hw_mosi = spi_mosi<0 ? HW_PIN_DATASPI : spi_mosi; - bool isHW = (oldPin[0]==hw_sclk && oldPin[1]==hw_mosi); - if (isHW) po = PinOwner::HW_SPI; - } else { - uint8_t hw_scl = i2c_scl<0 ? HW_PIN_SCL : i2c_scl; - uint8_t hw_sda = i2c_sda<0 ? HW_PIN_SDA : i2c_sda; - bool isHW = (oldPin[0]==hw_scl && oldPin[1]==hw_sda); - if (isHW) po = PinOwner::HW_I2C; - } - pinManager.deallocateMultiplePins((const uint8_t *)oldPin, 2, po); - type = newType; - setup(); - needsRedraw |= true; - } else { - u8x8->setBusClock(ioFrequency); // can be used for SPI too - setVcomh(contrastFix); - setContrast(contrast); - setFlipMode(flip); - } - knownHour = 99; - if (needsRedraw && !wakeDisplay()) redraw(true); - else overlayLogo(3500); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_contrastFix)].isNull(); - } + bool readFromConfig(JsonObject& root); /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). @@ -1219,4 +358,1021 @@ const char FourLineDisplayUsermod::_showSeconds[] PROGMEM = "showSeconds"; const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; const char FourLineDisplayUsermod::_contrastFix[] PROGMEM = "contrastFix"; +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) FourLineDisplayUsermod *FourLineDisplayUsermod::instance = nullptr; +#endif + +// some displays need this to properly apply contrast +void FourLineDisplayUsermod::setVcomh(bool highContrast) { + if (type == NONE || !enabled) return; + u8x8_t *u8x8_struct = u8x8->getU8x8(); + u8x8_cad_StartTransfer(u8x8_struct); + u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value + u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64 + u8x8_cad_EndTransfer(u8x8_struct); +} + +void FourLineDisplayUsermod::startDisplay() { + if (type == NONE || !enabled) return; + lineHeight = u8x8->getRows() > 4 ? 2 : 1; + DEBUG_PRINTLN(F("Starting display.")); + u8x8->setBusClock(ioFrequency); // can be used for SPI too + u8x8->begin(); + setFlipMode(flip); + setVcomh(contrastFix); + setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 + setPowerSave(0); + //drawString(0, 0, "Loading..."); + overlayLogo(3500); +} + +/** + * Wrappers for screen drawing + */ +void FourLineDisplayUsermod::setFlipMode(uint8_t mode) { + if (type == NONE || !enabled) return; + u8x8->setFlipMode(mode); +} +void FourLineDisplayUsermod::setContrast(uint8_t contrast) { + if (type == NONE || !enabled) return; + u8x8->setContrast(contrast); +} +void FourLineDisplayUsermod::drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(u8x8_font_chroma48medium8_r); + if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); + else u8x8->drawString(col, row, string); + drawing = false; +} +void FourLineDisplayUsermod::draw2x2String(uint8_t col, uint8_t row, const char *string) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(u8x8_font_chroma48medium8_r); + u8x8->draw2x2String(col, row, string); + drawing = false; +} +void FourLineDisplayUsermod::drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(font); + if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); + else u8x8->drawGlyph(col, row, glyph); + drawing = false; +} +void FourLineDisplayUsermod::draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->setFont(font); + u8x8->draw2x2Glyph(col, row, glyph); + drawing = false; +} +uint8_t FourLineDisplayUsermod::getCols() { + if (type==NONE || !enabled) return 0; + return u8x8->getCols(); +} +void FourLineDisplayUsermod::clear() { + if (type == NONE || !enabled) return; + drawing = true; + u8x8->clear(); + drawing = false; +} +void FourLineDisplayUsermod::setPowerSave(uint8_t save) { + if (type == NONE || !enabled) return; + u8x8->setPowerSave(save); +} + +void FourLineDisplayUsermod::center(String &line, uint8_t width) { + int len = line.length(); + if (len0; i--) line = ' ' + line; + for (byte i=line.length(); i 11) { AmPmHour -= 12; isitAM = false; } + if (AmPmHour == 0) { AmPmHour = 12; } + } + if (knownHour != hourCurrent) { + // only update date when hour changes + sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); + draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day + } + sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); + draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds + if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time + + drawStatusIcons(); //icons power, wifi, timer, etc + + knownMinute = minuteCurrent; + knownHour = hourCurrent; + } + if (showSeconds && secondCurrent != lastSecond) { + lastSecond = secondCurrent; + draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":"); + sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); + drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line + } +} + +/** + * Enable sleep (turn the display off) or clock mode. + */ +void FourLineDisplayUsermod::sleepOrClock(bool enabled) { + if (enabled) { + displayTurnedOff = true; + if (clockMode && ntpEnabled) { + knownMinute = knownHour = 99; + showTime(); + } else + setPowerSave(1); + } else { + displayTurnedOff = false; + setPowerSave(0); + } +} + +// gets called once at boot. Do all initialization that doesn't depend on +// network here +void FourLineDisplayUsermod::setup() { + bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); + + // check if pins are -1 and disable usermod as PinManager::allocateMultiplePins() will accept -1 as a valid pin + if (isSPI) { + if (spi_sclk<0 || spi_mosi<0 || ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { + type = NONE; + } else { + PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; + if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; } + } + } else { + if (i2c_scl<0 || i2c_sda<0) { type=NONE; } + } + + DEBUG_PRINTLN(F("Allocating display.")); + switch (type) { + // U8X8 uses Wire (or Wire1 with 2ND constructor) and will use existing Wire properties (calls Wire.begin() though) + case SSD1306: u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(); break; + case SH1106: u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(); break; + case SSD1306_64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(); break; + case SSD1305: u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(); break; + case SSD1305_64: u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(); break; + // U8X8 uses global SPI variable that is attached to VSPI bus on ESP32 + case SSD1306_SPI: u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset + case SSD1306_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset + case SSD1309_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset + // catchall + default: u8x8 = (U8X8 *) new U8X8_NULL(); enabled = false; break; // catchall to create U8x8 instance + } + + if (nullptr == u8x8) { + DEBUG_PRINTLN(F("Display init failed.")); + if (isSPI) { + pinManager.deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay); + } + type = NONE; + return; + } + + startDisplay(); + onUpdateBegin(false); // create Display task + initDone = true; +} + +// gets called every time WiFi is (re-)connected. Initialize own network +// interfaces here +void FourLineDisplayUsermod::connected() { + knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : + knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); + networkOverlay(PSTR("NETWORK INFO"),7000); +} + +/** + * Da loop. + */ +void FourLineDisplayUsermod::loop() { +#if !(defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)) + if (!enabled || strip.isUpdating()) return; + unsigned long now = millis(); + if (now < nextUpdate) return; + nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate); + redraw(false); +#endif +} + +/** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ +void FourLineDisplayUsermod::redraw(bool forceRedraw) { + bool needRedraw = false; + unsigned long now = millis(); + + if (type == NONE || !enabled) return; + if (overlayUntil > 0) { + if (now >= overlayUntil) { + // Time to display the overlay has elapsed. + overlayUntil = 0; + forceRedraw = true; + } else { + // We are still displaying the overlay + // Don't redraw. + return; + } + } + + while (drawing && millis()-now < 25) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; + + if (apActive && WLED_WIFI_CONFIGURED && now<15000) { + knownSsid = apSSID; + networkOverlay(PSTR("NETWORK INFO"),30000); + return; + } + + // Check if values which are shown on display changed from the last time. + if (forceRedraw) { + needRedraw = true; + clear(); + } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon + powerON = !powerON; + drawStatusIcons(); + return; + } else if (knownnightlight != nightlightActive) { //trigger moon icon + knownnightlight = nightlightActive; + drawStatusIcons(); + if (knownnightlight) { + String timer = PSTR("Timer On"); + center(timer,LINE_BUFFER_SIZE-1); + overlay(timer.c_str(), 2500, 6); + } + return; + } else if (wificonnected != interfacesInited) { //trigger wifi icon + wificonnected = interfacesInited; + drawStatusIcons(); + return; + } else if (knownMode != effectCurrent || knownPalette != effectPalette) { + if (displayTurnedOff) needRedraw = true; + else { + if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } + if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } + lastRedraw = now; + return; + } + } else if (knownBrightness != bri) { + if (displayTurnedOff && nightlightActive) { knownBrightness = bri; } + else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; } + } else if (knownEffectSpeed != effectSpeed) { + if (displayTurnedOff) needRedraw = true; + else { updateSpeed(); lastRedraw = now; return; } + } else if (knownEffectIntensity != effectIntensity) { + if (displayTurnedOff) needRedraw = true; + else { updateIntensity(); lastRedraw = now; return; } + } + + if (!needRedraw) { + // Nothing to change. + // Turn off display after 1 minutes with no change. + if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { + // We will still check if there is a change in redraw() + // and turn it back on if it changed. + clear(); + sleepOrClock(true); + } else if (displayTurnedOff && ntpEnabled) { + showTime(); + } + return; + } + + lastRedraw = now; + + // Turn the display back on + wakeDisplay(); + + // Update last known values. + knownBrightness = bri; + knownMode = effectCurrent; + knownPalette = effectPalette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownnightlight = nightlightActive; + wificonnected = interfacesInited; + + // Do the actual drawing + // First row: Icons + draw2x2GlyphIcons(); + drawArrow(); + drawStatusIcons(); + + // Second row + updateBrightness(); + updateSpeed(); + updateIntensity(); + + // Third row + showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info + + // Fourth row + showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info +} + +void FourLineDisplayUsermod::updateBrightness() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + knownBrightness = bri; + if (overlayUntil == 0) { + lockRedraw = true; + brightness100 = ((uint16_t)bri*100)/255; + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), brightness100); + drawString(1, lineHeight, lineBuffer); + lockRedraw = false; + } +} + +void FourLineDisplayUsermod::updateSpeed() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + knownEffectSpeed = effectSpeed; + if (overlayUntil == 0) { + lockRedraw = true; + fxspeed100 = ((uint16_t)effectSpeed*100)/255; + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), fxspeed100); + drawString(5, lineHeight, lineBuffer); + lockRedraw = false; + } +} + +void FourLineDisplayUsermod::updateIntensity() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + knownEffectIntensity = effectIntensity; + if (overlayUntil == 0) { + lockRedraw = true; + fxintensity100 = ((uint16_t)effectIntensity*100)/255; + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), fxintensity100); + drawString(9, lineHeight, lineBuffer); + lockRedraw = false; + } +} + +void FourLineDisplayUsermod::drawStatusIcons() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + uint8_t col = 15; + uint8_t row = 0; + lockRedraw = true; + drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon + if (lineHeight==2) { col--; } else { row++; } + drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon + if (lineHeight==2) { col--; } else { col = row = 0; } + drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode + lockRedraw = false; +} + +/** + * marks the position of the arrow showing + * the current setting being changed + * pass line and colum info + */ +void FourLineDisplayUsermod::setMarkLine(byte newMarkLineNum, byte newMarkColNum) { + markLineNum = newMarkLineNum; + markColNum = newMarkColNum; +} + +//Draw the arrow for the current setting beiong changed +void FourLineDisplayUsermod::drawArrow() { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1); + lockRedraw = false; +} + +//Display the current effect or palette (desiredEntry) +// on the appropriate line (row). +void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + char lineBuffer[MAX_JSON_CHARS]; + if (overlayUntil == 0) { + lockRedraw = true; + // Find the mode name in JSON + uint8_t printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1); + if (lineBuffer[0]=='*' && lineBuffer[1]==' ') { + // remove "* " from dynamic palettes + for (byte i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\0' + printedChars -= 2; + } else if ((lineBuffer[0]==' ' && lineBuffer[1]>127)) { + // remove note symbol from effect names + for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' + printedChars -= 5; + } + if (lineHeight == 2) { // use this code for 8 line display + char smallBuffer1[MAX_MODE_LINE_SPACE]; + char smallBuffer2[MAX_MODE_LINE_SPACE]; + uint8_t smallChars1 = 0; + uint8_t smallChars2 = 0; + if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits + while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; + lineBuffer[printedChars] = 0; + drawString(1, row*lineHeight, lineBuffer); + } else { // for long names divide the text into 2 lines and print them small + bool spaceHit = false; + for (uint8_t i = 0; i < printedChars; i++) { + switch (lineBuffer[i]) { + case ' ': + if (i > 4 && !spaceHit) { + spaceHit = true; + break; + } + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + else smallBuffer1[smallChars1++] = lineBuffer[i]; + break; + default: + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + else smallBuffer1[smallChars1++] = lineBuffer[i]; + break; + } + } + while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; + smallBuffer1[smallChars1] = 0; + drawString(1, row*lineHeight, smallBuffer1, true); + while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; + smallBuffer2[smallChars2] = 0; + drawString(1, row*lineHeight+1, smallBuffer2, true); + } + } else { // use this code for 4 ling displays + char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette + uint8_t smallChars3 = 0; + for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; + smallBuffer3[smallChars3] = 0; + drawString(1, row*lineHeight, smallBuffer3, true); + } + lockRedraw = false; + } +} + +/** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ +bool FourLineDisplayUsermod::wakeDisplay() { + if (type == NONE || !enabled) return false; + if (displayTurnedOff) { + #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return false; + #endif + lockRedraw = true; + clear(); + // Turn the display back on + sleepOrClock(false); + lockRedraw = false; + return true; + } + return false; +} + +/** + * Allows you to show one line and a glyph as overlay for a period of time. + * Clears the screen and prints. + * Used in Rotary Encoder usermod. + */ +void FourLineDisplayUsermod::overlay(const char* line1, long showHowLong, byte glyphType) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (glyphType>0 && glyphType<255) { + if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font + else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true); + } + if (line1) { + String buf = line1; + center(buf, getCols()); + drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str()); + } + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + +/** + * Allows you to show Akemi WLED logo overlay for a period of time. + * Clears the screen and prints. + */ +void FourLineDisplayUsermod::overlayLogo(long showHowLong) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (lineHeight == 2) { + //add a bit of randomness + switch (millis()%3) { + case 0: + //WLED + draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2); + draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2); + draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2); + draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2); + break; + case 1: + //WLED Akemi + drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true); + drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true); + drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true); + break; + case 2: + //Akemi + //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font + drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true); + drawString(6, 6, "WLED"); + break; + } + } else { + switch (millis()%3) { + case 0: + //WLED + draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2); + draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2); + draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2); + draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2); + break; + case 1: + //WLED Akemi + drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4); + drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4); + drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4); + break; + case 2: + //Akemi + //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash + draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2); + break; + } + } + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + +/** + * Allows you to show two lines as overlay for a period of time. + * Clears the screen and prints. + * Used in Auto Save usermod + */ +void FourLineDisplayUsermod::overlay(const char* line1, const char* line2, long showHowLong) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (line1) { + String buf = line1; + center(buf, getCols()); + drawString(0, 1*lineHeight, buf.c_str()); + } + if (line2) { + String buf = line2; + center(buf, getCols()); + drawString(0, 2*lineHeight, buf.c_str()); + } + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + +void FourLineDisplayUsermod::networkOverlay(const char* line1, long showHowLong) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing + if (drawing || lockRedraw) return; +#endif + lockRedraw = true; + + String line; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (line1) { + line = line1; + center(line, getCols()); + drawString(0, 0, line.c_str()); + } + // Second row with Wifi name + line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); + if (line.length() < getCols()) center(line, getCols()); + drawString(0, lineHeight, line.c_str()); + // Print `~` char to indicate that SSID is longer, than our display + if (knownSsid.length() > getCols()) { + drawString(getCols() - 1, 0, "~"); + } + // Third row with IP and Password in AP Mode + line = knownIp.toString(); + center(line, getCols()); + drawString(0, lineHeight*2, line.c_str()); + line = ""; + if (apActive) { + line = apPass; + } else if (strcmp(serverDescription, "WLED") != 0) { + line = serverDescription; + } + center(line, getCols()); + drawString(0, lineHeight*3, line.c_str()); + overlayUntil = millis() + showHowLong; + lockRedraw = false; +} + + +/** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ +bool FourLineDisplayUsermod::handleButton(uint8_t b) { + yield(); + if (!enabled + || b // butto 0 only + || buttonType[b] == BTN_TYPE_SWITCH + || 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; + } + + unsigned long now = millis(); + static bool buttonPressedBefore = false; + static bool buttonLongPressed = false; + static unsigned long buttonPressedTime = 0; + static unsigned long buttonWaitTime = 0; + bool handled = true; + + //momentary button logic + if (isButtonPressed(b)) { //pressed + + if (!buttonPressedBefore) buttonPressedTime = now; + buttonPressedBefore = true; + + if (now - buttonPressedTime > 600) { //long press + buttonLongPressed = true; + //TODO: handleButton() handles button 0 without preset in a different way for double click + //so we need to override with same behaviour + longPressAction(0); + //handled = false; + } + + } else if (!isButtonPressed(b) && buttonPressedBefore) { //released + + long dur = now - buttonPressedTime; + if (dur < 50) { + buttonPressedBefore = false; + return true; + } //too short "press", debounce + + bool doublePress = buttonWaitTime; //did we have short press before? + buttonWaitTime = 0; + + if (!buttonLongPressed) { //short press + // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + //TODO: handleButton() handles button 0 without preset in a different way for double click + if (doublePress) { + networkOverlay(PSTR("NETWORK INFO"),7000); + handled = true; + } else { + buttonWaitTime = now; + } + } + buttonPressedBefore = false; + buttonLongPressed = false; + } + // if 350ms elapsed since last press/release it is a short press + if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) { + buttonWaitTime = 0; + //TODO: handleButton() handles button 0 without preset in a different way for double click + //so we need to override with same behaviour + shortPressAction(0); + //handled = false; + } + return handled; +} + +#if CONFIG_FREERTOS_UNICORE +#define ARDUINO_RUNNING_CORE 0 +#else +#define ARDUINO_RUNNING_CORE 1 +#endif +void FourLineDisplayUsermod::onUpdateBegin(bool init) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + if (init && Display_Task) { + vTaskSuspend(Display_Task); // update is about to begin, disable task to prevent crash + } else { + // update has failed or create task requested + if (Display_Task) + vTaskResume(Display_Task); + else + xTaskCreatePinnedToCore( + [](void * par) { // Function to implement the task + // see https://www.freertos.org/vtaskdelayuntil.html + const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; + TickType_t xLastWakeTime = xTaskGetTickCount(); + for(;;) { + delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. + // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. + vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, by doing nothing for REFRESH_RATE_MS millis + FourLineDisplayUsermod::getInstance()->redraw(false); + } + }, + "4LD", // Name of the task + 3072, // Stack size in words + NULL, // Task input parameter + 1, // Priority of the task (not idle) + &Display_Task, // Task handle + ARDUINO_RUNNING_CORE + ); + } +#endif +} + +/* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ +//void FourLineDisplayUsermod::addToJsonInfo(JsonObject& root) { + //JsonObject user = root["u"]; + //if (user.isNull()) user = root.createNestedObject("u"); + //JsonArray data = user.createNestedArray(F("4LineDisplay")); + //data.add(F("Loaded.")); +//} + +/* + * 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 FourLineDisplayUsermod::addToJsonState(JsonObject& root) { +//} + +/* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ +//void FourLineDisplayUsermod::readFromJsonState(JsonObject& root) { +// if (!initDone) return; // prevent crash on boot applyPreset() +//} + +void FourLineDisplayUsermod::appendConfigData() { + oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'SSD1306',1);")); + oappend(SET_F("addOption(dd,'SH1106',2);")); + oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); + oappend(SET_F("addOption(dd,'SSD1305',4);")); + oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); + oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); + oappend(SET_F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); +} + +/* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ +void FourLineDisplayUsermod::addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + + top["type"] = type; + JsonArray io_pin = top.createNestedArray("pin"); + for (int i=0; i<3; i++) io_pin.add(ioPin[i]); + top[FPSTR(_flip)] = (bool) flip; + top[FPSTR(_contrast)] = contrast; + top[FPSTR(_contrastFix)] = (bool) contrastFix; + #ifndef ARDUINO_ARCH_ESP32 + top[FPSTR(_refreshRate)] = refreshRate; + #endif + top[FPSTR(_screenTimeOut)] = screenTimeout/1000; + top[FPSTR(_sleepMode)] = (bool) sleepMode; + top[FPSTR(_clockMode)] = (bool) clockMode; + top[FPSTR(_showSeconds)] = (bool) showSeconds; + top[FPSTR(_busClkFrequency)] = ioFrequency/1000; + DEBUG_PRINTLN(F("4 Line Display config saved.")); +} + +/* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ +bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { + bool needsRedraw = false; + DisplayType newType = type; + int8_t oldPin[3]; for (byte i=0; i<3; i++) oldPin[i] = ioPin[i]; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + newType = top["type"] | newType; + for (byte i=0; i<3; i++) ioPin[i] = top["pin"][i] | ioPin[i]; + flip = top[FPSTR(_flip)] | flip; + contrast = top[FPSTR(_contrast)] | contrast; + #ifndef ARDUINO_ARCH_ESP32 + refreshRate = top[FPSTR(_refreshRate)] | refreshRate; + refreshRate = min(5000, max(250, (int)refreshRate)); + #endif + screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; + sleepMode = top[FPSTR(_sleepMode)] | sleepMode; + clockMode = top[FPSTR(_clockMode)] | clockMode; + showSeconds = top[FPSTR(_showSeconds)] | showSeconds; + contrastFix = top[FPSTR(_contrastFix)] | contrastFix; + if (newType == SSD1306_SPI || newType == SSD1306_SPI64) + ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency + else + ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + type = newType; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + bool pinsChanged = false; + for (byte i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } + if (pinsChanged || type!=newType) { + bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); + bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64); + if (isSPI) { + if (pinsChanged || !newSPI) pinManager.deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); + if (!newSPI) { + // was SPI but is no longer SPI + if (i2c_scl<0 || i2c_sda<0) { newType=NONE; } + } else { + // still SPI but pins changed + PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; + if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } + else if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + } + } else if (newSPI) { + // was I2C but is now SPI + if (spi_sclk<0 || spi_mosi<0) { + newType=NONE; + } else { + PinManagerPinType pins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; + if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } + else if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + } + } else { + // just I2C type changed + } + type = newType; + switch (type) { + case SSD1306: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SH1106: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1306_64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1305: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1305_64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; + case SSD1306_SPI: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset + break; + case SSD1306_SPI64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset + break; + case SSD1309_SPI64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset + default: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_null_cb, u8x8_cad_empty, u8x8_byte_empty, u8x8_dummy_cb); + enabled = false; + break; + } + startDisplay(); + needsRedraw |= true; + } else { + u8x8->setBusClock(ioFrequency); // can be used for SPI too + setVcomh(contrastFix); + setContrast(contrast); + setFlipMode(flip); + } + knownHour = 99; + if (needsRedraw && !wakeDisplay()) redraw(true); + else overlayLogo(3500); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_contrastFix)].isNull(); +} diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 35d3e6f3..b142f903 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -45,7 +45,27 @@ #define ENCODER_SW_PIN 19 #endif -// The last UI state, remove color and saturation option if diplay not active(too many options) +#ifndef ENCODER_MAX_DELAY_MS // max delay between polling encoder pins +#define ENCODER_MAX_DELAY_MS 8 // 8 milliseconds => max 120 change impulses in 1 second, for full turn of a 30/30 encoder (4 changes per segment, 30 segments for one turn) +#endif + +#ifndef USERMOD_USE_PCF8574 + #undef USE_PCF8574 + #define USE_PCF8574 false +#else + #undef USE_PCF8574 + #define USE_PCF8574 true +#endif + +#ifndef PCF8574_ADDRESS + #define PCF8574_ADDRESS 0x20 // some may start at 0x38 +#endif + +#ifndef PCF8574_INT_PIN + #define PCF8574_INT_PIN -1 // GPIO connected to INT pin on PCF8574 +#endif + +// The last UI state, remove color and saturation option if display not active (too many options) #ifdef USERMOD_FOUR_LINE_DISPLAY #define LAST_UI_STATE 11 #else @@ -113,166 +133,370 @@ static int re_qstringCmp(const void *ap, const void *bp) { } +static volatile uint8_t pcfPortData = 0; // port expander port state +static volatile uint8_t addrPcf8574 = PCF8574_ADDRESS; // has to be accessible in ISR + +// Interrupt routine to read I2C rotary state +// if we are to use PCF8574 port expander we will need to rely on interrupts as polling I2C every 2ms +// is a waste of resources and causes 4LD to fail. +// in such case rely on ISR to read pin values and store them into static variable +static void IRAM_ATTR i2cReadingISR() { + Wire.requestFrom(addrPcf8574, 1U); + if (Wire.available()) { + pcfPortData = Wire.read(); + } +} + + class RotaryEncoderUIUsermod : public Usermod { -private: - int8_t fadeAmount = 5; // Amount to change every step (brightness) - unsigned long loopTime; - unsigned long buttonPressedTime = 0; - unsigned long buttonWaitTime = 0; - bool buttonPressedBefore = false; - bool buttonLongPressed = false; + private: - int8_t pinA = ENCODER_DT_PIN; // DT from encoder - int8_t pinB = ENCODER_CLK_PIN; // CLK from encoder - int8_t pinC = ENCODER_SW_PIN; // SW from encoder + const int8_t fadeAmount; // Amount to change every step (brightness) + unsigned long loopTime; - unsigned char select_state = 0; // 0: brightness, 1: effect, 2: effect speed, ... + unsigned long buttonPressedTime; + unsigned long buttonWaitTime; + bool buttonPressedBefore; + bool buttonLongPressed; - uint16_t currentHue1 = 16; // default boot color - byte currentSat1 = 255; - -#ifdef USERMOD_FOUR_LINE_DISPLAY - FourLineDisplayUsermod *display; -#else - void* display = nullptr; + int8_t pinA; // DT from encoder + int8_t pinB; // CLK from encoder + int8_t pinC; // SW from encoder + + unsigned char select_state; // 0: brightness, 1: effect, 2: effect speed, ... + + uint16_t currentHue1; // default boot color + byte currentSat1; + uint8_t currentCCT; + + #ifdef USERMOD_FOUR_LINE_DISPLAY + FourLineDisplayUsermod *display; + #else + void* display; + #endif + + // Pointers the start of the mode names within JSON_mode_names + const char **modes_qstrings; + + // Array of mode indexes in alphabetical order. + byte *modes_alpha_indexes; + + // Pointers the start of the palette names within JSON_palette_names + const char **palettes_qstrings; + + // Array of palette indexes in alphabetical order. + byte *palettes_alpha_indexes; + + struct { // reduce memory footprint + bool Enc_A : 1; + bool Enc_B : 1; + bool Enc_A_prev : 1; + }; + + bool currentEffectAndPaletteInitialized; + uint8_t effectCurrentIndex; + uint8_t effectPaletteIndex; + uint8_t knownMode; + uint8_t knownPalette; + + byte presetHigh; + byte presetLow; + + bool applyToAll; + + bool initDone; + bool enabled; + + bool usePcf8574; + int8_t pinIRQ; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _DT_pin[]; + static const char _CLK_pin[]; + static const char _SW_pin[]; + static const char _presetHigh[]; + static const char _presetLow[]; + static const char _applyToAll[]; + static const char _pcf8574[]; + static const char _pcfAddress[]; + static const char _pcfINTpin[]; + + /** + * readPin() - read rotary encoder pin value + */ + byte readPin(uint8_t pin); + + /** + * Sort the modes and palettes to the index arrays + * modes_alpha_indexes and palettes_alpha_indexes. + */ + void sortModesAndPalettes(); + byte *re_initIndexArray(int numModes); + + /** + * Return an array of mode or palette names from the JSON string. + * They don't end in '\0', they end in '"'. + */ + const char **re_findModeStrings(const char json[], int numModes); + + /** + * Sort either the modes or the palettes using quicksort. + */ + void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip); + + public: + + RotaryEncoderUIUsermod() + : fadeAmount(5) + , buttonPressedTime(0) + , buttonWaitTime(0) + , buttonPressedBefore(false) + , buttonLongPressed(false) + , pinA(ENCODER_DT_PIN) + , pinB(ENCODER_CLK_PIN) + , pinC(ENCODER_SW_PIN) + , select_state(0) + , currentHue1(16) + , currentSat1(255) + , currentCCT(128) + , display(nullptr) + , modes_qstrings(nullptr) + , modes_alpha_indexes(nullptr) + , palettes_qstrings(nullptr) + , palettes_alpha_indexes(nullptr) + , currentEffectAndPaletteInitialized(false) + , effectCurrentIndex(0) + , effectPaletteIndex(0) + , knownMode(0) + , knownPalette(0) + , presetHigh(0) + , presetLow(0) + , applyToAll(true) + , initDone(false) + , enabled(true) + , usePcf8574(USE_PCF8574) + , pinIRQ(PCF8574_INT_PIN) + {} + + /* + * 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_ROTARY_ENC_UI; } + /** + * Enable/Disable the usermod + */ + inline void enable(bool enable) { if (!(pinA<0 || pinB<0 || pinC<0)) enabled = enable; } + + /** + * Get usermod enabled/disabled state + */ + inline bool isEnabled() { return enabled; } + + /** + * 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(); + + /** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + //void connected(); + + /** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop(); + +#ifndef WLED_DISABLE_MQTT + //bool onMqttMessage(char* topic, char* payload); + //void onMqttConnect(bool sessionPresent); #endif - // Pointers the start of the mode names within JSON_mode_names - const char **modes_qstrings = nullptr; + /** + * 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); - // Array of mode indexes in alphabetical order. - byte *modes_alpha_indexes = nullptr; + /** + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + */ + //void addToJsonInfo(JsonObject &root); - // Pointers the start of the palette names within JSON_palette_names - const char **palettes_qstrings = nullptr; + /** + * 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); - // Array of palette indexes in alphabetical order. - byte *palettes_alpha_indexes = nullptr; + /** + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void readFromJsonState(JsonObject &root); - unsigned char Enc_A; - unsigned char Enc_B; - unsigned char Enc_A_prev = 0; + /** + * provide the changeable values + */ + void addToConfig(JsonObject &root); - bool currentEffectAndPaletteInitialized = false; - uint8_t effectCurrentIndex = 0; - uint8_t effectPaletteIndex = 0; - uint8_t knownMode = 0; - uint8_t knownPalette = 0; + void appendConfigData(); - uint8_t currentCCT = 128; + /** + * 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 readFromConfig(JsonObject &root); - byte presetHigh = 0; - byte presetLow = 0; + // custom methods + void displayNetworkInfo(); + void findCurrentEffectAndPalette(); + bool changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph); + void lampUdated(); + void changeBrightness(bool increase); + void changeEffect(bool increase); + void changeEffectSpeed(bool increase); + void changeEffectIntensity(bool increase); + void changeCustom(uint8_t par, bool increase); + void changePalette(bool increase); + void changeHue(bool increase); + void changeSat(bool increase); + void changePreset(bool increase); + void changeCCT(bool increase); +}; - bool applyToAll = true; - bool initDone = false; - bool enabled = true; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _DT_pin[]; - static const char _CLK_pin[]; - static const char _SW_pin[]; - static const char _presetHigh[]; - static const char _presetLow[]; - static const char _applyToAll[]; - - /** - * Sort the modes and palettes to the index arrays - * modes_alpha_indexes and palettes_alpha_indexes. - */ - void sortModesAndPalettes() { - //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); - modes_qstrings = strip.getModeDataSrc(); - modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); - re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); - - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); // only use internal palettes - - // How many palette names start with '*' and should not be sorted? - // (Also skipping the first one, 'Default'). - int skipPaletteCount = 1; - while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount++]) == '*') ; - re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); +/** + * readPin() - read rotary encoder pin value + */ +byte RotaryEncoderUIUsermod::readPin(uint8_t pin) { + if (usePcf8574) { + if (pin >= 100) pin -= 100; // PCF I/O ports + return (pcfPortData>>pin) & 1; + } else { + return digitalRead(pin); } +} - byte *re_initIndexArray(int numModes) { - byte *indexes = (byte *)malloc(sizeof(byte) * numModes); - for (byte i = 0; i < numModes; i++) { - indexes[i] = i; +/** + * Sort the modes and palettes to the index arrays + * modes_alpha_indexes and palettes_alpha_indexes. + */ +void RotaryEncoderUIUsermod::sortModesAndPalettes() { + DEBUG_PRINTLN(F("Sorting modes and palettes.")); + //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); + modes_qstrings = strip.getModeDataSrc(); + modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); + re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); + + palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); + palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); // only use internal palettes + + // How many palette names start with '*' and should not be sorted? + // (Also skipping the first one, 'Default'). + int skipPaletteCount = 1; + while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount++]) == '*') ; + re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); +} + +byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) { + byte *indexes = (byte *)malloc(sizeof(byte) * numModes); + for (byte i = 0; i < numModes; i++) { + indexes[i] = i; + } + return indexes; +} + +/** + * Return an array of mode or palette names from the JSON string. + * They don't end in '\0', they end in '"'. + */ +const char **RotaryEncoderUIUsermod::re_findModeStrings(const char json[], int numModes) { + const char **modeStrings = (const char **)malloc(sizeof(const char *) * numModes); + uint8_t modeIndex = 0; + bool insideQuotes = false; + // advance past the mark for markLineNum that may exist. + char singleJsonSymbol; + + // Find the mode name in JSON + bool complete = false; + for (size_t i = 0; i < strlen_P(json); i++) { + singleJsonSymbol = pgm_read_byte_near(json + i); + if (singleJsonSymbol == '\0') break; + switch (singleJsonSymbol) { + case '"': + insideQuotes = !insideQuotes; + if (insideQuotes) { + // We have a new mode or palette + modeStrings[modeIndex] = (char *)(json + i + 1); + } + break; + case '[': + break; + case ']': + if (!insideQuotes) complete = true; + break; + case ',': + if (!insideQuotes) modeIndex++; + default: + if (!insideQuotes) break; } - return indexes; + if (complete) break; } + return modeStrings; +} - /** - * Return an array of mode or palette names from the JSON string. - * They don't end in '\0', they end in '"'. - */ - const char **re_findModeStrings(const char json[], int numModes) { - const char **modeStrings = (const char **)malloc(sizeof(const char *) * numModes); - uint8_t modeIndex = 0; - bool insideQuotes = false; - // advance past the mark for markLineNum that may exist. - char singleJsonSymbol; +/** + * Sort either the modes or the palettes using quicksort. + */ +void RotaryEncoderUIUsermod::re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip) { + if (!modeNames) return; + listBeingSorted = modeNames; + qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp); + listBeingSorted = nullptr; +} - // Find the mode name in JSON - bool complete = false; - for (size_t i = 0; i < strlen_P(json); i++) { - singleJsonSymbol = pgm_read_byte_near(json + i); - if (singleJsonSymbol == '\0') break; - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - if (insideQuotes) { - // We have a new mode or palette - modeStrings[modeIndex] = (char *)(json + i + 1); - } - break; - case '[': - break; - case ']': - if (!insideQuotes) complete = true; - break; - case ',': - if (!insideQuotes) modeIndex++; - default: - if (!insideQuotes) break; + +// public methods + + +/* + * 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 RotaryEncoderUIUsermod::setup() +{ + DEBUG_PRINTLN(F("Usermod Rotary Encoder init.")); + + if (usePcf8574) { + if (i2c_sda < 0 || i2c_scl < 0 || pinA < 0 || pinB < 0 || pinC < 0) { + DEBUG_PRINTLN(F("I2C and/or PCF8574 pins unused, disabling.")); + enabled = false; + return; + } else { + if (pinIRQ >= 0 && pinManager.allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { + pinMode(pinIRQ, INPUT_PULLUP); + attachInterrupt(pinIRQ, i2cReadingISR, FALLING); // RISING, FALLING, CHANGE, ONLOW, ONHIGH + DEBUG_PRINTLN(F("Interrupt attached.")); + } else { + DEBUG_PRINTLN(F("Unable to allocate interrupt pin, disabling.")); + pinIRQ = -1; + enabled = false; + return; } - if (complete) break; } - return modeStrings; - } - - /** - * Sort either the modes or the palettes using quicksort. - */ - void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip) { - if (!modeNames) return; - listBeingSorted = modeNames; - qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp); - listBeingSorted = nullptr; - } - - -public: - /* - * 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() - { - DEBUG_PRINTLN(F("Usermod Rotary Encoder init.")); + } else { PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { - // BUG: configuring this usermod with conflicting pins - // will cause it to de-allocate pins it does not own - // (at second config) - // This is the exact type of bug solved by pinManager - // tracking the owner tags.... pinA = pinB = pinC = -1; enabled = false; return; @@ -284,638 +508,648 @@ public: pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO); pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO); pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO); + } - loopTime = millis(); + loopTime = millis(); - currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; + currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; - if (!initDone) sortModesAndPalettes(); + if (!initDone) sortModesAndPalettes(); #ifdef USERMOD_FOUR_LINE_DISPLAY - // This Usermod uses FourLineDisplayUsermod for the best experience. - // But it's optional. But you want it. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); - if (display != nullptr) { - display->setMarkLine(1, 0); - } + // This Usermod uses FourLineDisplayUsermod for the best experience. + // But it's optional. But you want it. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + if (display != nullptr) { + display->setMarkLine(1, 0); + } #endif - initDone = true; - Enc_A = digitalRead(pinA); // Read encoder pins - Enc_B = digitalRead(pinB); - Enc_A_prev = Enc_A; + initDone = true; + Enc_A = readPin(pinA); // Read encoder pins + Enc_B = readPin(pinB); + Enc_A_prev = Enc_A; +} + +/* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ +void RotaryEncoderUIUsermod::loop() +{ + if (!enabled) return; + unsigned long currentTime = millis(); // get the current elapsed time + if (strip.isUpdating() && ((currentTime - loopTime) < ENCODER_MAX_DELAY_MS)) return; // be nice, but not too nice + + // Initialize effectCurrentIndex and effectPaletteIndex to + // current state. We do it here as (at least) effectCurrent + // is not yet initialized when setup is called. + + if (!currentEffectAndPaletteInitialized) { + findCurrentEffectAndPalette(); } - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() + if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) { + DEBUG_PRINTLN(F("Current mode or palette changed.")); + currentEffectAndPaletteInitialized = false; + } + + if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz { - //Serial.println("Connected to WiFi!"); + bool buttonPressed = !readPin(pinC); //0=pressed, 1=released + if (buttonPressed) { + if (!buttonPressedBefore) buttonPressedTime = currentTime; + buttonPressedBefore = true; + if (currentTime-buttonPressedTime > 3000) { + if (!buttonLongPressed) displayNetworkInfo(); //long press for network info + buttonLongPressed = true; + } + } else if (!buttonPressed && buttonPressedBefore) { + bool doublePress = buttonWaitTime; + buttonWaitTime = 0; + if (!buttonLongPressed) { + if (doublePress) { + toggleOnOff(); + lampUdated(); + } else { + buttonWaitTime = currentTime; + } + } + buttonLongPressed = false; + buttonPressedBefore = false; + } + if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp + buttonWaitTime = 0; + char newState = select_state + 1; + bool changedState = false; + char lineBuffer[64]; + do { + // finde new state + switch (newState) { + case 0: strcpy_P(lineBuffer, PSTR("Brightness")); changedState = true; break; + case 1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; // speed + case 2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; // intensity + case 3: strcpy_P(lineBuffer, PSTR("Color Palette")); changedState = true; break; + case 4: strcpy_P(lineBuffer, PSTR("Effect")); changedState = true; break; + case 5: strcpy_P(lineBuffer, PSTR("Main Color")); changedState = true; break; + case 6: strcpy_P(lineBuffer, PSTR("Saturation")); changedState = true; break; + case 7: + if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++; + else { strcpy_P(lineBuffer, PSTR("CCT")); changedState = true; } + break; + case 8: if (presetHigh==0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR("Preset")); changedState = true; } break; + case 9: + case 10: + case 11: if (!extractModeSlider(effectCurrent, newState-7, lineBuffer, 63)) newState++; else changedState = true; break; // custom + } + if (newState > LAST_UI_STATE) newState = 0; + } while (!changedState); + if (display != nullptr) { + switch (newState) { + case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun + case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward + case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire + case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette + case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece + case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush + case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast + case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart + case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + } + } + if (changedState) select_state = newState; + } + + Enc_A = readPin(pinA); // Read encoder pins + Enc_B = readPin(pinB); + if ((Enc_A) && (!Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse + { // B is high so clockwise + switch(select_state) { + case 0: changeBrightness(true); break; + case 1: changeEffectSpeed(true); break; + case 2: changeEffectIntensity(true); break; + case 3: changePalette(true); break; + case 4: changeEffect(true); break; + case 5: changeHue(true); break; + case 6: changeSat(true); break; + case 7: changeCCT(true); break; + case 8: changePreset(true); break; + case 9: changeCustom(1,true); break; + case 10: changeCustom(2,true); break; + case 11: changeCustom(3,true); break; + } + } + else if (Enc_B == HIGH) + { // B is low so counter-clockwise + switch(select_state) { + case 0: changeBrightness(false); break; + case 1: changeEffectSpeed(false); break; + case 2: changeEffectIntensity(false); break; + case 3: changePalette(false); break; + case 4: changeEffect(false); break; + case 5: changeHue(false); break; + case 6: changeSat(false); break; + case 7: changeCCT(false); break; + case 8: changePreset(false); break; + case 9: changeCustom(1,false); break; + case 10: changeCustom(2,false); break; + case 11: changeCustom(3,false); break; + } + } + } + Enc_A_prev = Enc_A; // Store value of A for next time + loopTime = currentTime; // Updates loopTime } +} - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - * - * Tips: - * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. - * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. - * - * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. - * Instead, use a timer check as shown here. - */ - void loop() - { - if (!enabled || strip.isUpdating()) return; - unsigned long currentTime = millis(); // get the current elapsed time - - // Initialize effectCurrentIndex and effectPaletteIndex to - // current state. We do it here as (at least) effectCurrent - // is not yet initialized when setup is called. - - if (!currentEffectAndPaletteInitialized) { - findCurrentEffectAndPalette(); - } - - if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) { - currentEffectAndPaletteInitialized = false; - } - - if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz - { - loopTime = currentTime; // Updates loopTime - - bool buttonPressed = !digitalRead(pinC); //0=pressed, 1=released - if (buttonPressed) { - if (!buttonPressedBefore) buttonPressedTime = currentTime; - buttonPressedBefore = true; - if (currentTime-buttonPressedTime > 3000) { - if (!buttonLongPressed) displayNetworkInfo(); //long press for network info - buttonLongPressed = true; - } - } else if (!buttonPressed && buttonPressedBefore) { - bool doublePress = buttonWaitTime; - buttonWaitTime = 0; - if (!buttonLongPressed) { - if (doublePress) { - toggleOnOff(); - lampUdated(); - } else { - buttonWaitTime = currentTime; - } - } - buttonLongPressed = false; - buttonPressedBefore = false; - } - if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp - buttonWaitTime = 0; - char newState = select_state + 1; - bool changedState = false; - char lineBuffer[64]; - do { - // finde new state - switch (newState) { - case 0: strcpy_P(lineBuffer, PSTR("Brightness")); changedState = true; break; - case 1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; // speed - case 2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; // intensity - case 3: strcpy_P(lineBuffer, PSTR("Color Palette")); changedState = true; break; - case 4: strcpy_P(lineBuffer, PSTR("Effect")); changedState = true; break; - case 5: strcpy_P(lineBuffer, PSTR("Main Color")); changedState = true; break; - case 6: strcpy_P(lineBuffer, PSTR("Saturation")); changedState = true; break; - case 7: - if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++; - else { strcpy_P(lineBuffer, PSTR("CCT")); changedState = true; } - break; - case 8: if (presetHigh==0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR("Preset")); changedState = true; } break; - case 9: - case 10: - case 11: if (!extractModeSlider(effectCurrent, newState-7, lineBuffer, 63)) newState++; else changedState = true; break; // custom - } - if (newState > LAST_UI_STATE) newState = 0; - } while (!changedState); - if (display != nullptr) { - switch (newState) { - case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun - case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward - case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire - case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette - case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece - case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush - case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast - case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart - case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - } - } - if (changedState) select_state = newState; - } - - Enc_A = digitalRead(pinA); // Read encoder pins - Enc_B = digitalRead(pinB); - if ((Enc_A) && (!Enc_A_prev)) - { // A has gone from high to low - if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse - { // B is high so clockwise - switch(select_state) { - case 0: changeBrightness(true); break; - case 1: changeEffectSpeed(true); break; - case 2: changeEffectIntensity(true); break; - case 3: changePalette(true); break; - case 4: changeEffect(true); break; - case 5: changeHue(true); break; - case 6: changeSat(true); break; - case 7: changeCCT(true); break; - case 8: changePreset(true); break; - case 9: changeCustom(1,true); break; - case 10: changeCustom(2,true); break; - case 11: changeCustom(3,true); break; - } - } - else if (Enc_B == HIGH) - { // B is low so counter-clockwise - switch(select_state) { - case 0: changeBrightness(false); break; - case 1: changeEffectSpeed(false); break; - case 2: changeEffectIntensity(false); break; - case 3: changePalette(false); break; - case 4: changeEffect(false); break; - case 5: changeHue(false); break; - case 6: changeSat(false); break; - case 7: changeCCT(false); break; - case 8: changePreset(false); break; - case 9: changeCustom(1,false); break; - case 10: changeCustom(2,false); break; - case 11: changeCustom(3,false); break; - } - } - } - Enc_A_prev = Enc_A; // Store value of A for next time - } - } - - void displayNetworkInfo() { - #ifdef USERMOD_FOUR_LINE_DISPLAY - display->networkOverlay(PSTR("NETWORK INFO"), 10000); - #endif - } - - void findCurrentEffectAndPalette() { - currentEffectAndPaletteInitialized = true; - for (uint8_t i = 0; i < strip.getModeCount(); i++) { - if (modes_alpha_indexes[i] == effectCurrent) { - effectCurrentIndex = i; - break; - } - } - - for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { - if (palettes_alpha_indexes[i] == effectPalette) { - effectPaletteIndex = i; - break; - } - } - } - - boolean changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { +void RotaryEncoderUIUsermod::displayNetworkInfo() { #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display != nullptr) { - if (display->wakeDisplay()) { - // Throw away wake up input - display->redraw(true); - return false; - } - display->overlay(stateName, 750, glyph); - display->setMarkLine(markedLine, markedCol); - } + display->networkOverlay(PSTR("NETWORK INFO"), 10000); #endif - return true; - } +} - void lampUdated() { - //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) - // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa - //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required) - stateUpdated(CALL_MODE_BUTTON); - updateInterfaces(CALL_MODE_BUTTON); +void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { + DEBUG_PRINTLN(F("Finding current mode and palette.")); + currentEffectAndPaletteInitialized = true; + for (uint8_t i = 0; i < strip.getModeCount(); i++) { + if (modes_alpha_indexes[i] == effectCurrent) { + effectCurrentIndex = i; + break; + } } + DEBUG_PRINTLN(F("Found current mode.")); - void changeBrightness(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); + for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { + if (palettes_alpha_indexes[i] == effectPalette) { + effectPaletteIndex = i; + break; + } + } + DEBUG_PRINTLN(F("Found palette.")); +} + +bool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + if (display->wakeDisplay()) { // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); - lampUdated(); - #ifdef USERMOD_FOUR_LINE_DISPLAY - display->updateBrightness(); - #endif - } - - - void changeEffect(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { display->redraw(true); - // Throw away wake up input - return; + return false; } - display->updateRedrawTime(); - #endif - effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); - effectCurrent = modes_alpha_indexes[effectCurrentIndex]; - stateChanged = true; - if (applyToAll) { - for (byte i=0; ioverlay(stateName, 750, glyph); + display->setMarkLine(markedLine, markedCol); + } +#endif + return true; +} + +void RotaryEncoderUIUsermod::lampUdated() { + //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) + // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa + //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required) + stateUpdated(CALL_MODE_BUTTON); + updateInterfaces(CALL_MODE_BUTTON); +} + +void RotaryEncoderUIUsermod::changeBrightness(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateBrightness(); +#endif +} + + +void RotaryEncoderUIUsermod::changeEffect(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); + effectCurrent = modes_alpha_indexes[effectCurrentIndex]; + stateChanged = true; + if (applyToAll) { + for (byte i=0; ishowCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.setMode(effectCurrent); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); +#endif +} - void changeEffectSpeed(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateSpeed(); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.speed = effectSpeed; } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateSpeed(); +#endif +} - void changeEffectIntensity(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateIntensity(); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.intensity = effectIntensity; } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateIntensity(); +#endif +} - void changeCustom(uint8_t par, bool increase) { - uint8_t val = 0; - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - stateChanged = true; - if (applyToAll) { - uint8_t id = strip.getFirstSelectedSegId(); - Segment& sid = strip.getSegment(id); - switch (par) { - case 3: val = sid.custom3 = max(min((increase ? sid.custom3+fadeAmount : sid.custom3-fadeAmount), 255), 0); break; - case 2: val = sid.custom2 = max(min((increase ? sid.custom2+fadeAmount : sid.custom2-fadeAmount), 255), 0); break; - default: val = sid.custom1 = max(min((increase ? sid.custom1+fadeAmount : sid.custom1-fadeAmount), 255), 0); break; - } - for (byte i=0; ioverlay(lineBuffer, 500, 10); // use star - #endif +void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) { + uint8_t val = 0; +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; } - - - void changePalette(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; + display->updateRedrawTime(); +#endif + stateChanged = true; + if (applyToAll) { + uint8_t id = strip.getFirstSelectedSegId(); + Segment& sid = strip.getSegment(id); + switch (par) { + case 3: val = sid.custom3 = max(min((increase ? sid.custom3+fadeAmount : sid.custom3-fadeAmount), 255), 0); break; + case 2: val = sid.custom2 = max(min((increase ? sid.custom2+fadeAmount : sid.custom2-fadeAmount), 255), 0); break; + default: val = sid.custom1 = max(min((increase ? sid.custom1+fadeAmount : sid.custom1-fadeAmount), 255), 0); break; } - display->updateRedrawTime(); - #endif - effectPaletteIndex = max(min((increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()-1), 0); - effectPalette = palettes_alpha_indexes[effectPaletteIndex]; - stateChanged = true; - if (applyToAll) { - for (byte i=0; ioverlay(lineBuffer, 500, 10); // use star +#endif +} + + +void RotaryEncoderUIUsermod::changePalette(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectPaletteIndex = max(min((increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()-1), 0); + effectPalette = palettes_alpha_indexes[effectPaletteIndex]; + stateChanged = true; + if (applyToAll) { + for (byte i=0; ishowCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.setPalette(effectPalette); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + display->showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); +#endif +} - void changeHue(bool increase){ - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); + colorHStoRGB(currentHue1*256, currentSat1, col); + stateChanged = true; + if (applyToAll) { + for (byte i=0; ioverlay(lineBuffer, 500, 7); // use brush - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.colors[0] = RGBW32(col[0], col[1], col[2], col[3]); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64]; + sprintf(lineBuffer, "%d", currentHue1); + display->overlay(lineBuffer, 500, 7); // use brush +#endif +} - void changeSat(bool increase){ - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); + colorHStoRGB(currentHue1*256, currentSat1, col); + if (applyToAll) { + for (byte i=0; ioverlay(lineBuffer, 500, 8); // use contrast - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.colors[0] = RGBW32(col[0], col[1], col[2], col[3]); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64]; + sprintf(lineBuffer, "%d", currentSat1); + display->overlay(lineBuffer, 500, 8); // use contrast +#endif +} - void changePreset(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - if (presetHigh && presetLow && presetHigh > presetLow) { - StaticJsonDocument<64> root; - char str[64]; - sprintf_P(str, PSTR("%d~%d~%s"), presetLow, presetHigh, increase?"":"-"); - root["ps"] = str; - deserializeState(root.as(), CALL_MODE_BUTTON_PRESET); +void RotaryEncoderUIUsermod::changePreset(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + if (presetHigh && presetLow && presetHigh > presetLow) { + StaticJsonDocument<64> root; + char str[64]; + sprintf_P(str, PSTR("%d~%d~%s"), presetLow, presetHigh, increase?"":"-"); + root["ps"] = str; + deserializeState(root.as(), CALL_MODE_BUTTON_PRESET); /* - String apireq = F("win&PL=~"); - if (!increase) apireq += '-'; - apireq += F("&P1="); - apireq += presetLow; - apireq += F("&P2="); - apireq += presetHigh; - handleSet(nullptr, apireq, false); + String apireq = F("win&PL=~"); + if (!increase) apireq += '-'; + apireq += F("&P1="); + apireq += presetLow; + apireq += F("&P2="); + apireq += presetHigh; + handleSet(nullptr, apireq, false); */ - lampUdated(); - #ifdef USERMOD_FOUR_LINE_DISPLAY - sprintf(str, "%d", currentPreset); - display->overlay(str, 500, 11); // use heart - #endif - } - } - - void changeCCT(bool increase){ + lampUdated(); #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); + sprintf(str, "%d", currentPreset); + display->overlay(str, 500, 11); // use heart #endif - currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); + } +} + +void RotaryEncoderUIUsermod::changeCCT(bool increase){ +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); // if (applyToAll) { - for (byte i=0; ioverlay(lineBuffer, 500, 10); // use star - #endif - } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64]; + sprintf(lineBuffer, "%d", currentCCT); + display->overlay(lineBuffer, 500, 10); // use star +#endif +} - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - /* - void addToJsonInfo(JsonObject& root) - { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - JsonArray lightArr = user.createNestedArray("Light"); //name - lightArr.add(reading); //value - lightArr.add(" lux"); //unit - } +/* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor */ +/* +void RotaryEncoderUIUsermod::addToJsonInfo(JsonObject& root) +{ + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit +} +*/ - /* - * 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) - { - //root["user0"] = userVar0; - } +/* + * 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 RotaryEncoderUIUsermod::addToJsonState(JsonObject &root) +{ + //root["user0"] = userVar0; +} +*/ - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - /* - void readFromJsonState(JsonObject &root) - { - //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value - //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); - } +/* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients */ +/* +void RotaryEncoderUIUsermod::readFromJsonState(JsonObject &root) +{ + //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); +} +*/ - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ - void addToConfig(JsonObject &root) { - // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_DT_pin)] = pinA; - top[FPSTR(_CLK_pin)] = pinB; - top[FPSTR(_SW_pin)] = pinC; - top[FPSTR(_presetLow)] = presetLow; - top[FPSTR(_presetHigh)] = presetHigh; - top[FPSTR(_applyToAll)] = applyToAll; - DEBUG_PRINTLN(F("Rotary Encoder config saved.")); - } +/** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ +void RotaryEncoderUIUsermod::addToConfig(JsonObject &root) { + // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_DT_pin)] = pinA; + top[FPSTR(_CLK_pin)] = pinB; + top[FPSTR(_SW_pin)] = pinC; + top[FPSTR(_presetLow)] = presetLow; + top[FPSTR(_presetHigh)] = presetHigh; + top[FPSTR(_applyToAll)] = applyToAll; + top[FPSTR(_pcf8574)] = usePcf8574; + top[FPSTR(_pcfAddress)] = addrPcf8574; + top[FPSTR(_pcfINTpin)] = pinIRQ; + DEBUG_PRINTLN(F("Rotary Encoder config saved.")); +} - /** - * 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 readFromConfig(JsonObject &root) { - // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; - int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; - int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; - - presetHigh = top[FPSTR(_presetHigh)] | presetHigh; - presetLow = top[FPSTR(_presetLow)] | presetLow; - presetHigh = MIN(250,MAX(0,presetHigh)); - presetLow = MIN(250,MAX(0,presetLow)); - - enabled = top[FPSTR(_enabled)] | enabled; - applyToAll = top[FPSTR(_applyToAll)] | applyToAll; +void RotaryEncoderUIUsermod::appendConfigData() { + oappend(SET_F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); + oappend(SET_F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); +} +/** + * 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 RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) { + // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json - pinA = newDTpin; - pinB = newCLKpin; - pinC = newSWpin; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; + int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; + int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; + int8_t newIRQpin = top[FPSTR(_pcfINTpin)] | pinIRQ; + bool oldPcf8574 = usePcf8574; + + presetHigh = top[FPSTR(_presetHigh)] | presetHigh; + presetLow = top[FPSTR(_presetLow)] | presetLow; + presetHigh = MIN(250,MAX(0,presetHigh)); + presetLow = MIN(250,MAX(0,presetLow)); + + enabled = top[FPSTR(_enabled)] | enabled; + applyToAll = top[FPSTR(_applyToAll)] | applyToAll; + + usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574; + addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574; + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + pinA = newDTpin; + pinB = newCLKpin; + pinC = newSWpin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin || pinIRQ!=newIRQpin) { + if (oldPcf8574) { + if (pinIRQ >= 0) { + detachInterrupt(pinIRQ); + pinManager.deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); + DEBUG_PRINTLN(F("Deallocated old IRQ pin.")); + } + pinIRQ = newIRQpin<100 ? newIRQpin : -1; // ignore PCF8574 pins + } else { pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); - pinA = newDTpin; - pinB = newCLKpin; - pinC = newSWpin; - if (pinA<0 || pinB<0 || pinC<0) { - enabled = false; - return true; - } - setup(); + DEBUG_PRINTLN(F("Deallocated old pins.")); } + pinA = newDTpin; + pinB = newCLKpin; + pinC = newSWpin; + if (pinA<0 || pinB<0 || pinC<0) { + enabled = false; + return true; + } + setup(); } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_applyToAll)].isNull(); } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_pcfINTpin)].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_ROTARY_ENC_UI; - } -}; // strings to reduce flash memory usage (used more than twice) const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; @@ -926,3 +1160,6 @@ const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; const char RotaryEncoderUIUsermod::_presetHigh[] PROGMEM = "preset-high"; const char RotaryEncoderUIUsermod::_presetLow[] PROGMEM = "preset-low"; const char RotaryEncoderUIUsermod::_applyToAll[] PROGMEM = "apply-2-all-seg"; +const char RotaryEncoderUIUsermod::_pcf8574[] PROGMEM = "use-PCF8574"; +const char RotaryEncoderUIUsermod::_pcfAddress[] PROGMEM = "PCF8574-address"; +const char RotaryEncoderUIUsermod::_pcfINTpin[] PROGMEM = "PCF8574-INT-pin"; diff --git a/wled00.sln b/wled00.sln deleted file mode 100644 index b2f12b83..00000000 --- a/wled00.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28010.2046 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wled00", "wled00\wled00.vcxproj", "{C5F80730-F44F-4478-BDAE-6634EFC2CA88}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Debug|x86.ActiveCfg = Debug|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Debug|x86.Build.0 = Debug|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Release|x86.ActiveCfg = Release|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9A679C2B-61D3-400B-B96F-06E604E9CED2} - EndGlobalSection -EndGlobal diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5d0d30ba..533da32b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -278,7 +278,7 @@ uint16_t mode_random_color(void) { SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), fade)); return FRAMETIME; } -static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!"; +static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!;01"; /* @@ -433,7 +433,7 @@ uint16_t mode_rainbow(void) { return FRAMETIME; } -static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!"; +static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;01"; /* @@ -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 - const 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(); @@ -4175,11 +4175,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)); @@ -4593,7 +4591,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); @@ -5097,21 +5095,22 @@ uint16_t mode_2DLissajous(void) { // By: Andrew Tuline const uint16_t rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(SEGMENT.intensity); + uint_fast16_t phase = (millis() * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed //for (int i=0; i < 4*(cols+rows); i ++) { for (int i=0; i < 256; i ++) { //float xlocn = float(sin8(now/4+i*(SEGMENT.speed>>5))) / 255.0f; //float ylocn = float(cos8(now/4+i*2)) / 255.0f; - uint8_t xlocn = sin8(millis()/4+i*(SEGMENT.speed>>5)); - uint8_t ylocn = cos8(millis()/4+i*2); - xlocn = map(xlocn,0,255,0,cols-1); - ylocn = map(ylocn,0,255,0,rows-1); - SEGMENT.setPixelColorXY(xlocn, ylocn, SEGMENT.color_from_palette(millis()/100+i, false, PALETTE_SOLID_WRAP, 0)); + uint_fast8_t xlocn = sin8(phase/2 + (i*SEGMENT.speed)/32); + uint_fast8_t ylocn = cos8(phase/2 + i*2); + xlocn = (cols < 2) ? 1 : (map(2*xlocn, 0,511, 0,2*(cols-1)) +1) /2; // softhack007: "(2* ..... +1) /2" for proper rounding + ylocn = (rows < 2) ? 1 : (map(2*ylocn, 0,511, 0,2*(rows-1)) +1) /2; // "rows > 1" is needed to avoid div/0 in map() + SEGMENT.setPixelColorXY((uint8_t)xlocn, (uint8_t)ylocn, SEGMENT.color_from_palette(millis()/100+i, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; } // mode_2DLissajous() -static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate;!;!;2"; +static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,,,Speed;!;!;2;;c3=15"; /////////////////////// @@ -5275,7 +5274,7 @@ uint16_t mode_2DPlasmaball(void) { // By: Stepko https://edito SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); - float t = millis() / (33 - SEGMENT.speed/8); + uint_fast32_t t = (millis() * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { uint16_t thisVal = inoise8(i * 30, t, t); uint16_t thisMax = map(thisVal, 0, 255, 0, cols-1); @@ -5878,11 +5877,17 @@ uint16_t mode_2Dscrollingtext(void) { case 4: letterWidth = 7; letterHeight = 9; break; case 5: letterWidth = 5; letterHeight = 12; break; } + const bool zero = SEGMENT.check3; const int yoffset = map(SEGMENT.intensity, 0, 255, -rows/2, rows/2) + (rows-letterHeight)/2; - char text[33] = {'\0'}; + char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; if (SEGMENT.name) for (size_t i=0,j=0; i31 && SEGMENT.name[i]<128) text[j++] = SEGMENT.name[i]; - if (!strlen(text) || !strncmp_P(text,PSTR("#DATE"),5) || !strncmp_P(text,PSTR("#DDMM"),5) || !strncmp_P(text,PSTR("#MMDD"),5) || !strncmp_P(text,PSTR("#TIME"),5) || !strncmp_P(text,PSTR("#HHMM"),5)) { // fallback if empty segment name: display date and time + if (!strlen(text) + || !strncmp_P(text,PSTR("#DATE"),5) + || !strncmp_P(text,PSTR("#DDMM"),5) + || !strncmp_P(text,PSTR("#MMDD"),5) + || !strncmp_P(text,PSTR("#TIME"),5) + || !strncmp_P(text,PSTR("#HHMM"),5)) { // fallback if empty segment name: display date and time char sec[5]; byte AmPmHour = hour(localTime); boolean isitAM = true; @@ -5892,12 +5897,12 @@ uint16_t mode_2Dscrollingtext(void) { } if (useAMPM) sprintf_P(sec, PSTR(" %2s"), (isitAM ? "AM" : "PM")); else sprintf_P(sec, PSTR(":%02d"), second(localTime)); - if (!strncmp_P(text,PSTR("#DATE"),5)) sprintf_P(text, PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); - else if (!strncmp_P(text,PSTR("#DDMM"),5)) sprintf_P(text, PSTR("%d.%d"), day(localTime), month(localTime)); - else if (!strncmp_P(text,PSTR("#MMDD"),5)) sprintf_P(text, PSTR("%d/%d"), month(localTime), day(localTime)); - else if (!strncmp_P(text,PSTR("#TIME"),5)) sprintf_P(text, PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); - else if (!strncmp_P(text,PSTR("#HHMM"),5)) sprintf_P(text, PSTR("%2d:%02d"), AmPmHour, minute(localTime)); - else sprintf_P(text, PSTR("%s %d, %d %2d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); + if (!strncmp_P(text,PSTR("#DATE"),5)) sprintf_P(text, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); + else if (!strncmp_P(text,PSTR("#DDMM"),5)) sprintf_P(text, zero?PSTR("%02d.%02d"):PSTR("%d.%d"), day(localTime), month(localTime)); + else if (!strncmp_P(text,PSTR("#MMDD"),5)) sprintf_P(text, zero?PSTR("%02d/%02d"):PSTR("%d/%d"), month(localTime), day(localTime)); + else if (!strncmp_P(text,PSTR("#TIME"),5)) sprintf_P(text, zero?PSTR("%02d:%02d%s"):PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); + else if (!strncmp_P(text,PSTR("#HHMM"),5)) sprintf_P(text, zero?PSTR("%02d:%02d"):PSTR("%d:%02d"), AmPmHour, minute(localTime)); + else sprintf_P(text, zero?PSTR("%s %02d, %04d %02d:%02d%s"):PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); } const int numberOfLetters = strlen(text); @@ -5924,7 +5929,7 @@ uint16_t mode_2Dscrollingtext(void) { return FRAMETIME; } -static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,,Gradient,Overlay;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; +static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,,Gradient,Overlay,0;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; //////////////////////////// @@ -6835,7 +6840,7 @@ uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Plesch return FRAMETIME; } // mode_freqmatrix() -static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Time delay,Sound effect,Low bin,High bin,Sensivity;;;1f;m12=3,si=0"; // Corner, Beatsin +static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensivity;;;1f;m12=3,si=0"; // Corner, Beatsin ////////////////////// @@ -6939,7 +6944,7 @@ uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschun return FRAMETIME; } // mode_freqwave() -static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Time delay,Sound effect,Low bin,High bin,Pre-amp;;;1f;m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;1f;m12=2,si=0"; // Circle, Beatsin /////////////////////// diff --git a/wled00/FX.h b/wled00/FX.h index 745e10f7..4f7bcb88 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -353,7 +353,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; @@ -486,7 +487,7 @@ typedef struct Segment { //if (leds) Serial.printf(" [%u]", length()*sizeof(CRGB)); //Serial.println(); //#endif - if (!Segment::_globalLeds && leds) free(leds); + if (!Segment::_globalLeds && leds) { free(leds); leds = nullptr;} // reset to nullptr, to avoid race conditions if (name) delete[] name; if (_t) delete _t; deallocateData(); @@ -506,16 +507,16 @@ typedef struct Segment { inline bool hasRGB(void) const { return _isRGB; } inline bool hasWhite(void) const { return _hasW; } inline bool isCCT(void) const { return _isCCT; } - inline uint16_t width(void) const { return stop - start; } // segment width in physical pixels (length if 1D) - inline uint16_t height(void) const { return stopY - startY; } // segment height (if 2D) in physical pixels - inline uint16_t length(void) const { return width() * height(); } // segment length (count) in physical pixels + inline uint16_t width(void) const { return (stop > start) ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) + inline uint16_t height(void) const { return (stopY > startY) ? (stopY - startY) : 0; } // segment height (if 2D) in physical pixels // softhack007: make sure its always > 0 + inline uint16_t length(void) const { return width() * height(); } // segment length (count) in physical pixels inline uint16_t groupLength(void) const { return grouping + spacing; } inline uint8_t getLightCapabilities(void) const { return _capabilities; } static uint16_t getUsedSegmentData(void) { return _usedSegmentData; } static void addUsedSegmentData(int len) { _usedSegmentData += len; } - 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); @@ -709,7 +710,7 @@ class WS2812FX { // 96 bytes panel.clear(); #endif customPalettes.clear(); - if (useLedsArray && Segment::_globalLeds) free(Segment::_globalLeds); + if (useLedsArray && Segment::_globalLeds) {free(Segment::_globalLeds); Segment::_globalLeds = nullptr;} // reset to nullptr, to avoid race conditions } static WS2812FX* getInstance(void) { return instance; } @@ -771,6 +772,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_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 8b4c70ef..28c35fa4 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -191,6 +191,8 @@ uint32_t WS2812FX::getPixelColorXY(uint16_t x, uint16_t y) { uint16_t /*IRAM_ATTR*/ Segment::XY(uint16_t x, uint16_t y) { uint16_t width = virtualWidth(); // segment width in logical pixels uint16_t height = virtualHeight(); // segment height in logical pixels + if (width == 0) return 0; // softhack007 avoid div/0 + if (height == 0) return (x%width); // softhack007 avoid div/0 return (x%width) + (y%height) * width; } @@ -202,7 +204,6 @@ void /*IRAM_ATTR*/ Segment::setPixelColorXY(int x, int y, uint32_t col) if (leds) leds[XY(x,y)] = col; uint8_t _bri_t = currentBri(on ? opacity : 0); - if (!_bri_t && !transitional) return; if (_bri_t < 255) { byte r = scale8(R(col), _bri_t); byte g = scale8(G(col), _bri_t); @@ -305,6 +306,7 @@ void Segment::blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t // Adds the specified color with the existing pixel color perserving color balance. void Segment::addPixelColorXY(int x, int y, uint32_t color, bool fast) { + if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit uint32_t col = getPixelColorXY(x,y); uint8_t r = R(col); uint8_t g = G(col); @@ -329,50 +331,54 @@ void Segment::fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { // blurRow: perform a blur on a row of a rectangular matrix void Segment::blurRow(uint16_t row, fract8 blur_amount) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); if (row >= rows) return; // blur one row uint8_t keep = 255 - blur_amount; uint8_t seep = blur_amount >> 1; CRGB carryover = CRGB::Black; - for (uint16_t x = 0; x < cols; x++) { + for (uint_fast16_t x = 0; x < cols; x++) { CRGB cur = getPixelColorXY(x, row); + CRGB before = cur; // remember color before blur CRGB part = cur; part.nscale8(seep); cur.nscale8(keep); cur += carryover; - if (x) { + if (x>0) { CRGB prev = CRGB(getPixelColorXY(x-1, row)) + part; setPixelColorXY(x-1, row, prev); } - setPixelColorXY(x, row, cur); + if (before != cur) // optimization: only set pixel if color has changed + setPixelColorXY(x, row, cur); carryover = part; } } // blurCol: perform a blur on a column of a rectangular matrix void Segment::blurCol(uint16_t col, fract8 blur_amount) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); if (col >= cols) return; // blur one column uint8_t keep = 255 - blur_amount; uint8_t seep = blur_amount >> 1; CRGB carryover = CRGB::Black; - for (uint16_t i = 0; i < rows; i++) { - CRGB cur = getPixelColorXY(col, i); + for (uint_fast16_t y = 0; y < rows; y++) { + CRGB cur = getPixelColorXY(col, y); CRGB part = cur; + CRGB before = cur; // remember color before blur part.nscale8(seep); cur.nscale8(keep); cur += carryover; - if (i) { - CRGB prev = CRGB(getPixelColorXY(col, i-1)) + part; - setPixelColorXY(col, i-1, prev); + if (y>0) { + CRGB prev = CRGB(getPixelColorXY(col, y-1)) + part; + setPixelColorXY(col, y-1, prev); } - setPixelColorXY(col, i, cur); + if (before != cur) // optimization: only set pixel if color has changed + setPixelColorXY(col, y, cur); carryover = part; } } @@ -391,8 +397,8 @@ void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { for (uint16_t j = 0; j < dim1; j++) { uint16_t x = vertical ? i : j; uint16_t y = vertical ? j : i; - uint16_t xp = vertical ? x : x-1; - uint16_t yp = vertical ? y-1 : y; + int16_t xp = vertical ? x : x-1; // "signed" to prevent underflow + int16_t yp = vertical ? y-1 : y; // "signed" to prevent underflow uint16_t xn = vertical ? x : x+1; uint16_t yn = vertical ? y+1 : y; CRGB curr = getPixelColorXY(x,y); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index d3363cb9..8d91c056 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -90,18 +90,18 @@ Segment::Segment(const Segment &orig) { if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } if (orig._t) { _t = new Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } - if (orig.leds && !Segment::_globalLeds) { leds = (CRGB*)malloc(sizeof(CRGB)*length()); if (leds) memcpy(leds, orig.leds, sizeof(CRGB)*length()); } + if (orig.leds && !Segment::_globalLeds && length() > 0) { leds = (CRGB*)malloc(sizeof(CRGB)*length()); if (leds) memcpy(leds, orig.leds, sizeof(CRGB)*length()); } } // move constructor Segment::Segment(Segment &&orig) noexcept { //DEBUG_PRINTLN(F("-- Move segment constructor --")); memcpy((void*)this, (void*)&orig, sizeof(Segment)); + orig.leds = nullptr; orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; orig._t = nullptr; - orig.leds = nullptr; } // copy assignment @@ -111,7 +111,7 @@ Segment& Segment::operator= (const Segment &orig) { // clean destination if (name) delete[] name; if (_t) delete _t; - if (leds && !Segment::_globalLeds) free(leds); + if (leds && !Segment::_globalLeds) {free(leds); leds=nullptr;} deallocateData(); // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); @@ -125,7 +125,7 @@ Segment& Segment::operator= (const Segment &orig) { if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } if (orig._t) { _t = new Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } - if (orig.leds && !Segment::_globalLeds) { leds = (CRGB*)malloc(sizeof(CRGB)*length()); if (leds) memcpy(leds, orig.leds, sizeof(CRGB)*length()); } + if (orig.leds && !Segment::_globalLeds && length() > 0) { leds = (CRGB*)malloc(sizeof(CRGB)*length()); if (leds) memcpy(leds, orig.leds, sizeof(CRGB)*length()); } } return *this; } @@ -137,7 +137,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept { if (name) delete[] name; // free old name deallocateData(); // free old runtime data if (_t) delete _t; - if (leds && !Segment::_globalLeds) free(leds); + if (leds && !Segment::_globalLeds) {free(leds); leds=nullptr;} memcpy((void*)this, (void*)&orig, sizeof(Segment)); orig.name = nullptr; orig.data = nullptr; @@ -152,12 +152,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(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); @@ -184,7 +184,7 @@ void Segment::deallocateData() { void Segment::resetIfRequired() { if (reset) { if (leds && !Segment::_globalLeds) { free(leds); leds = nullptr; } - if (transitional && _t) { transitional = false; delete _t; _t = nullptr; } + //if (transitional && _t) { transitional = false; delete _t; _t = nullptr; } deallocateData(); next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; // setOption(SEG_OPTION_RESET, false); @@ -305,23 +305,26 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { } void Segment::startTransition(uint16_t dur) { - if (transitional || _t) return; // already in transition no need to store anything + if (!dur) { + transitional = false; + if (_t) { + delete _t; + _t = nullptr; + } + return; + } + if (transitional && _t) return; // already in transition no need to store anything // starting a transition has to occur before change so we get current values 1st - uint8_t _briT = currentBri(on ? opacity : 0); - uint8_t _cctT = currentBri(cct, true); - CRGBPalette16 _palT = CRGBPalette16(DEFAULT_COLOR); loadPalette(_palT, palette); - uint8_t _modeP = mode; - uint32_t _colorT[NUM_COLORS]; - for (size_t i=0; i_briT = _briT; - _t->_cctT = _cctT; + + CRGBPalette16 _palT = CRGBPalette16(DEFAULT_COLOR); loadPalette(_palT, palette); + _t->_briT = on ? opacity : 0; + _t->_cctT = cct; _t->_palT = _palT; - _t->_modeP = _modeP; - for (size_t i=0; i_colorT[i] = _colorT[i]; + _t->_modeP = mode; + for (size_t i=0; i_colorT[i] = colors[i]; transitional = true; // setOption(SEG_OPTION_TRANSITIONAL, true); } @@ -334,10 +337,10 @@ uint16_t Segment::progress() { } uint8_t Segment::currentBri(uint8_t briNew, bool useCct) { - if (transitional && _t) { - uint32_t prog = progress() + 1; - if (useCct) return ((briNew * prog) + _t->_cctT * (0x10000 - prog)) >> 16; - else return ((briNew * prog) + _t->_briT * (0x10000 - prog)) >> 16; + uint32_t prog = progress(); + if (transitional && _t && prog < 0xFFFFU) { + if (useCct) return ((briNew * prog) + _t->_cctT * (0xFFFFU - prog)) >> 16; + else return ((briNew * prog) + _t->_briT * (0xFFFFU - prog)) >> 16; } else { return briNew; } @@ -367,19 +370,18 @@ CRGBPalette16 &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal void Segment::handleTransition() { if (!transitional) return; - unsigned long maxWait = millis() + 20; - if (mode == FX_MODE_STATIC && next_time > maxWait) next_time = maxWait; - if (progress() == 0xFFFFU) { - if (_t) { - if (_t->_modeP != mode) markForReset(); + uint16_t _progress = progress(); + if (_progress == 0xFFFFU) transitional = false; // finish transitioning segment + if (_t) { // thanks to @nXm AKA https://github.com/NMeirer + if (_progress >= 32767U && _t->_modeP != mode) markForReset(); + if (_progress == 0xFFFFU) { delete _t; _t = nullptr; } - transitional = false; // finish transitioning segment } } -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 @@ -474,7 +476,7 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); - sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 7); + sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 1); sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; @@ -545,6 +547,7 @@ uint16_t Segment::virtualLength() const { } #endif uint16_t groupLen = groupLength(); + if (groupLen < 1) groupLen = 1; // prevent division by zero - better safe than sorry ... uint16_t vLength = (length() + groupLen - 1) / groupLen; if (mirror) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED return vLength; @@ -623,7 +626,6 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) uint16_t len = length(); uint8_t _bri_t = currentBri(on ? opacity : 0); - if (!_bri_t && !transitional && fadeTransition) return; // if _bri_t == 0 && segment is not transitionig && transitions are enabled then save a few CPU cycles if (_bri_t < 255) { byte r = scale8(R(col), _bri_t); byte g = scale8(G(col), _bri_t); @@ -728,7 +730,7 @@ uint32_t Segment::getPixelColor(int i) i += start; /* offset/phase */ i += offset; - if (i >= stop) i -= length(); + if ((i >= stop) && (stop>0)) i -= length(); // avoids negative pixel index (stop = 0 is a possible value) return strip.getPixelColor(i); } @@ -750,7 +752,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; @@ -885,6 +887,7 @@ void Segment::fade_out(uint8_t rate) { // fades all pixels to black using nscale8() void Segment::fadeToBlackBy(uint8_t fadeBy) { + if (fadeBy == 0) return; // optimization - no scaling to apply const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); const uint16_t rows = virtualHeight(); // will be 1 for 1D @@ -899,23 +902,26 @@ void Segment::fadeToBlackBy(uint8_t fadeBy) { */ void Segment::blur(uint8_t blur_amount) { + if (blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for (uint16_t i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows - for (uint16_t k = 0; k < cols; k++) blurCol(k, blur_amount); // blur all columns + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); + for (uint_fast16_t i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows + for (uint_fast16_t k = 0; k < cols; k++) blurCol(k, blur_amount); // blur all columns return; } #endif uint8_t keep = 255 - blur_amount; uint8_t seep = blur_amount >> 1; CRGB carryover = CRGB::Black; - for(uint16_t i = 0; i < virtualLength(); i++) + uint_fast16_t vlength = virtualLength(); + for(uint_fast16_t i = 0; i < vlength; i++) { CRGB cur = CRGB(getPixelColor(i)); CRGB part = cur; + CRGB before = cur; // remember color before blur part.nscale8(seep); cur.nscale8(keep); cur += carryover; @@ -924,9 +930,10 @@ void Segment::blur(uint8_t blur_amount) uint8_t r = R(c); uint8_t g = G(c); uint8_t b = B(c); - setPixelColor(i-1, qadd8(r, part.red), qadd8(g, part.green), qadd8(b, part.blue)); + setPixelColor((uint16_t)(i-1), qadd8(r, part.red), qadd8(g, part.green), qadd8(b, part.blue)); } - setPixelColor(i,cur.red, cur.green, cur.blue); + if (before != cur) // optimization: only set pixel if color has changed + setPixelColor((uint16_t)i,cur.red, cur.green, cur.blue); carryover = part; } } @@ -1076,7 +1083,7 @@ void WS2812FX::finalizeInit(void) //else //#endif Segment::_globalLeds = (CRGB*) malloc(arrSize); - memset(Segment::_globalLeds, 0, arrSize); + if (Segment::_globalLeds && (arrSize > 0)) memset(Segment::_globalLeds, 0, arrSize); } //segments are created in makeAutoSegments(); @@ -1095,6 +1102,8 @@ void WS2812FX::service() { _isServicing = true; _segment_index = 0; for (segment &seg : _segments) { + // process transition (mode changes in the middle of transition) + seg.handleTransition(); // reset the segment runtime data if needed seg.resetIfRequired(); @@ -1123,8 +1132,6 @@ void WS2812FX::service() { delay = (*_mode[seg.currentMode(seg.mode)])(); if (seg.mode != FX_MODE_HALLOWEEN_EYES) seg.call++; if (seg.transitional && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition - - seg.handleTransition(); } seg.next_time = nowUp + delay; @@ -1221,7 +1228,8 @@ void WS2812FX::estimateCurrentAndLimitBri() { } uint32_t powerSum0 = powerSum; - powerSum *= _brightness; + //powerSum *= _brightness; // for NPBrightnessBus + powerSum *= 255; // no need to scale down powerSum - NPB-LG getPixelColor returns colors scaled down by brightness if (powerSum > powerBudget) //scale brightness down to stay in current limit { @@ -1229,11 +1237,14 @@ void WS2812FX::estimateCurrentAndLimitBri() { uint16_t scaleI = scale * 255; uint8_t scaleB = (scaleI > 255) ? 255 : scaleI; uint8_t newBri = scale8(_brightness, scaleB); - busses.setBrightness(newBri); //to keep brightness uniform, sets virtual busses too - currentMilliamps = (powerSum0 * newBri) / puPerMilliamp; + // to keep brightness uniform, sets virtual busses too - softhack007: apply reductions immediately + if (scaleB < 255) busses.setBrightness(scaleB, true); // NPB-LG has already applied brightness, so its suffifient to post-apply scaling ==> use scaleB instead of newBri + busses.setBrightness(newBri, false); // set new brightness for next frame + //currentMilliamps = (powerSum0 * newBri) / puPerMilliamp; // for NPBrightnessBus + currentMilliamps = (powerSum0 * scaleB) / puPerMilliamp; // for NPBus-LG } else { currentMilliamps = powerSum / puPerMilliamp; - busses.setBrightness(_brightness); + busses.setBrightness(_brightness, false); // set new brightness for next frame } currentMilliamps += MA_FOR_ESP; //add power of ESP back to estimate currentMilliamps += pLen; //add standby power back to estimate @@ -1330,6 +1341,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; @@ -1427,7 +1446,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() { @@ -1583,7 +1602,7 @@ void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) { } void WS2812FX::setTransitionMode(bool t) { - for (segment &seg : _segments) if (!seg.transitional) seg.startTransition(t ? _transitionDur : 0); + for (segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } #ifdef WLED_DEBUG diff --git a/wled00/NodeStruct.h b/wled00/NodeStruct.h index 916c5a9a..2d4d3609 100644 --- a/wled00/NodeStruct.h +++ b/wled00/NodeStruct.h @@ -9,9 +9,9 @@ #include #define NODE_TYPE_ID_UNDEFINED 0 -#define NODE_TYPE_ID_ESP8266 82 -#define NODE_TYPE_ID_ESP32 32 -#define NODE_TYPE_ID_ESP32S2 33 +#define NODE_TYPE_ID_ESP8266 82 // should be 1 +#define NODE_TYPE_ID_ESP32 32 // should be 2 +#define NODE_TYPE_ID_ESP32S2 33 // etc #define NODE_TYPE_ID_ESP32S3 34 #define NODE_TYPE_ID_ESP32C3 35 @@ -23,7 +23,13 @@ struct NodeStruct String nodeName; IPAddress ip; uint8_t age; - uint8_t nodeType; + union { + uint8_t nodeType; // a waste of space as we only have 5 types + struct { + uint8_t type : 7; // still a waste of space (4 bits would be enough and future-proof) + bool on : 1; + }; + }; uint32_t build; NodeStruct() : age(0), nodeType(0), build(0) 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_manager.cpp b/wled00/bus_manager.cpp index 4b96060e..d11182aa 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -126,15 +126,15 @@ bool BusDigital::canShow() { return PolyBus::canShow(_busPtr, _iType); } -void BusDigital::setBrightness(uint8_t b) { +void BusDigital::setBrightness(uint8_t b, bool immediate) { //Fix for turning off onboard LED breaking bus #ifdef LED_BUILTIN if (_bri == 0 && b > 0) { if (_pins[0] == LED_BUILTIN || _pins[1] == LED_BUILTIN) PolyBus::begin(_busPtr, _iType, _pins); } #endif - Bus::setBrightness(b); - PolyBus::setBrightness(_busPtr, _iType, b); + Bus::setBrightness(b, immediate); + PolyBus::setBrightness(_busPtr, _iType, b, immediate); } //If LEDs are skipped, it is possible to use the first as a status LED. @@ -515,9 +515,9 @@ void IRAM_ATTR BusManager::setPixelColor(uint16_t pix, uint32_t c, int16_t cct) } } -void BusManager::setBrightness(uint8_t b) { +void BusManager::setBrightness(uint8_t b, bool immediate) { for (uint8_t i = 0; i < numBusses; i++) { - busses[i]->setBrightness(b); + busses[i]->setBrightness(b, immediate); } } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index b6d79d07..4a8da605 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -108,7 +108,7 @@ class Bus { virtual void setStatusPixel(uint32_t c) {} virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; virtual uint32_t getPixelColor(uint16_t pix) { return 0; } - virtual void setBrightness(uint8_t b) { _bri = b; }; + virtual void setBrightness(uint8_t b, bool immediate=false) { _bri = b; }; virtual void cleanup() = 0; virtual uint8_t getPins(uint8_t* pinArray) { return 0; } virtual uint16_t getLength() { return _len; } @@ -181,7 +181,7 @@ class BusDigital : public Bus { bool canShow(); - void setBrightness(uint8_t b); + void setBrightness(uint8_t b, bool immediate); void setStatusPixel(uint32_t c); @@ -345,7 +345,7 @@ class BusManager { void setPixelColor(uint16_t pix, uint32_t c, int16_t cct=-1); - void setBrightness(uint8_t b); + void setBrightness(uint8_t b, bool immediate=false); // immediate=true is for use in ABL, it applies brightness immediately (warning: inefficient) void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index c5c81298..b19997ce 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -122,6 +122,9 @@ #define I_SS_LPO_3 48 +// In the following NeoGammaNullMethod can be replaced with NeoGammaWLEDMethod to perform Gamma correction implicitly +// unfortunately that may apply Gamma correction to pre-calculated palettes which is undesired + /*** ESP8266 Neopixel methods ***/ #ifdef ESP8266 //RGB @@ -247,7 +250,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 @@ -799,101 +806,101 @@ class PolyBus { case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; } }; - static void setBrightness(void* busPtr, uint8_t busType, uint8_t b) { + static void setBrightness(void* busPtr, uint8_t busType, uint8_t b, bool immediate) { // immediate=true is for use in ABL, it applies brightness immediately (warning: inefficient) switch (busType) { case I_NONE: break; #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_I1_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_LPD_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_LPD_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_LPO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_LPO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_WS1_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_WS1_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_P98_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_P98_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; } }; static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { 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 316048be..15ef0e1f 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -297,9 +297,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } }; if (i2c_scl >= 0 && i2c_sda >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { #ifdef ESP32 - Wire.setPins(i2c_sda, i2c_scl); // this will fail if Wire is initilised (Wire.begin() called prior) + if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initilised (Wire.begin() called prior) + else Wire.begin(); + #else + Wire.begin(i2c_sda, i2c_scl); #endif - Wire.begin(); } else { i2c_sda = -1; i2c_scl = -1; @@ -336,7 +338,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (light_gc_col > 1.0f) gammaCorrectCol = true; else gammaCorrectCol = false; if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) { - if (gammaCorrectVal != 2.8f) calcGammaTable(gammaCorrectVal); + if (gammaCorrectVal != 2.8f) NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); } else { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; @@ -438,8 +440,16 @@ 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_ESPNOW + JsonObject remote = doc["remote"]; + CJSON(enable_espnow_remote, remote[F("remote_enabled")]); + getStringFromJson(linked_remote, remote[F("linked_remote")], 13); +#endif + + #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces["hue"]; CJSON(huePollingEnabled, if_hue["en"]); @@ -885,12 +895,20 @@ 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; if_mqtt_topics[F("group")] = mqttGroupTopic; #endif +#ifndef WLED_DISABLE_ESPNOW + JsonObject remote = doc.createNestedObject(F("remote")); + remote[F("remote_enabled")] = enable_espnow_remote; + remote[F("linked_remote")] = linked_remote; +#endif + + #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces.createNestedObject("hue"); if_hue["en"] = huePollingEnabled; diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 9874c314..8c4baabb 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -302,7 +302,7 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb) { } //gamma 2.8 lookup table used for color correction -static byte gammaT[] = { +uint8_t NeoGammaWLEDMethod::gammaT[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, @@ -320,27 +320,22 @@ static byte gammaT[] = { 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 }; -uint8_t gamma8_cal(uint8_t b, float gamma) -{ - return (int)(powf((float)b / 255.0f, gamma) * 255.0f + 0.5f); -} - // re-calculates & fills gamma table -void calcGammaTable(float gamma) +void NeoGammaWLEDMethod::calcGammaTable(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - gammaT[i] = gamma8_cal(i, gamma); + for (size_t i = 0; i < 256; i++) { + gammaT[i] = (int)(powf((float)i / 255.0f, gamma) * 255.0f + 0.5f); } } -// used for individual channel or brightness gamma correction -uint8_t gamma8(uint8_t b) +uint8_t NeoGammaWLEDMethod::Correct(uint8_t value) { - return gammaT[b]; + if (!gammaCorrectCol) return value; + return gammaT[value]; } // used for color gamma correction -uint32_t gamma32(uint32_t color) +uint32_t NeoGammaWLEDMethod::Correct32(uint32_t color) { if (!gammaCorrectCol) return color; uint8_t w = W(color); diff --git a/wled00/const.h b/wled00/const.h index f87db71d..91f3fde5 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 @@ -90,6 +91,21 @@ #endif #endif +#ifndef WLED_MAX_SEGNAME_LEN + #ifdef ESP8266 + #define WLED_MAX_SEGNAME_LEN 32 + #else + #define WLED_MAX_SEGNAME_LEN 64 + #endif +#else + #if WLED_MAX_SEGNAME_LEN<32 + #undef WLED_MAX_SEGNAME_LEN + #define WLED_MAX_SEGNAME_LEN 32 + #else + #warning WLED UI does not support modified maximum segment name length! + #endif +#endif + //Usermod IDs #define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present #define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID @@ -319,7 +335,8 @@ // WLED Error modes #define ERR_NONE 0 // All good :) -#define ERR_EEP_COMMIT 2 // Could not commit to EEPROM (wrong flash layout?) +#define ERR_DENIED 1 // Permission denied +#define ERR_EEP_COMMIT 2 // Could not commit to EEPROM (wrong flash layout?) OBSOLETE #define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) @@ -331,12 +348,29 @@ #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) -//Timer mode types +// Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness #define NL_MODE_FADE 1 //Fade to target brightness gradually #define NL_MODE_COLORFADE 2 //Fade to target brightness and secondary color gradually #define NL_MODE_SUN 3 //Sunrise/sunset. Target brightness is set immediately, then Sunrise effect is started. Max 60 min. +// Settings sub page IDs +#define SUBPAGE_MENU 0 +#define SUBPAGE_WIFI 1 +#define SUBPAGE_LEDS 2 +#define SUBPAGE_UI 3 +#define SUBPAGE_SYNC 4 +#define SUBPAGE_TIME 5 +#define SUBPAGE_SEC 6 +#define SUBPAGE_DMX 7 +#define SUBPAGE_UM 8 +#define SUBPAGE_UPDATE 9 +#define SUBPAGE_2D 10 +#define SUBPAGE_LOCK 251 +#define SUBPAGE_PINREQ 252 +#define SUBPAGE_CSS 253 +#define SUBPAGE_JS 254 +#define SUBPAGE_WELCOME 255 #define NTP_PACKET_SIZE 48 @@ -369,7 +403,7 @@ #ifdef ESP8266 #define SETTINGS_STACK_BUF_SIZE 2048 #else -#define SETTINGS_STACK_BUF_SIZE 3096 +#define SETTINGS_STACK_BUF_SIZE 3608 // warning: quite a large value for stack #endif #ifdef WLED_USE_ETHERNET @@ -441,7 +475,10 @@ #define DEFAULT_LED_COUNT 30 #endif -#define INTERFACE_UPDATE_COOLDOWN 2000 //time in ms to wait between websockets, alexa, and MQTT updates +#define INTERFACE_UPDATE_COOLDOWN 1000 // time in ms to wait between websockets, alexa, and MQTT updates + +#define PIN_RETRY_COOLDOWN 3000 // time in ms after an incorrect attempt PIN and OTA pass will be rejected even if correct +#define PIN_TIMEOUT 900000 // time in ms after which the PIN will be required again, 15 minutes // HW_PIN_SCL & HW_PIN_SDA are used for information in usermods settings page and usermods themselves // which GPIO pins are actually used in a hardwarea layout (controller board) diff --git a/wled00/data/404.htm b/wled00/data/404.htm index 260253a3..ff41fa6e 100644 --- a/wled00/data/404.htm +++ b/wled00/data/404.htm @@ -42,6 +42,6 @@

404 Not Found

Akemi does not know where you are headed...

- + \ No newline at end of file diff --git a/wled00/data/index.css b/wled00/data/index.css index fb8fad72..049e6f03 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -134,7 +134,7 @@ button { .off { color: var(--c-6) !important; - cursor: default !important; + /* cursor: default !important; */ } .top .icons, .bot .icons { @@ -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; @@ -387,6 +410,7 @@ button { position: -webkit-sticky; position: sticky; bottom: 0; + max-width: 300px; } #sliders .labels { @@ -395,7 +419,7 @@ button { } .slider { - max-width: 300px; + /*max-width: 300px;*/ /* margin: 5px auto; add 5px; if you want some vertical space but looks ugly */ border-radius: 24px; position: relative; @@ -411,12 +435,16 @@ button { z-index: 0; } +#sliders .slider { + padding-right: 64px; /* offset for bubble */ +} + #sliders .slider, #info .slider { background-color: var(--c-2); } #sliders .sliderwrap, .sbs .sliderwrap { - left: 16px; /* offset for icon */ + left: 32px; /* offset for icon */ } .filter, .option { @@ -482,13 +510,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 +588,6 @@ button { position: fixed; top: calc(var(--th) + 5px); left: 1px; - display: none; cursor: pointer; } @@ -662,24 +682,27 @@ 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; } .sliderbubble { width: 24px; - position: relative; + position: absolute; display: inline-block; - border-radius: 10px; + border-radius: 16px; background: var(--c-3); color: var(--c-f); - padding: 2px 4px; + padding: 4px; font-size: 14px; - right: 3px; - transition: visibility 0.25s ease, opacity 0.25s ease; + right: 6px; + transition: visibility .25s ease,opacity .25s ease; opacity: 0; visibility: hidden; - left: 8px; + /* left: 8px; */ + top: 4px; } output.sliderbubbleshow { @@ -771,8 +794,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); } @@ -803,6 +826,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; @@ -979,24 +1003,26 @@ textarea { } .ptxt { - margin: -1px 4px 8px !important; - display: none; + margin: -1px 4px 8px !important; } .stxt { width: 50px !important; } -.segname, .pname { +.segname, .pname, .bname { white-space: nowrap; text-align: center; - overflow: clip; + overflow: hidden; text-overflow: ellipsis; line-height: 24px; padding: 8px 24px; max-width: 170px; position: relative; } +.bname { + padding: 0 24px; +} .segname .flr, .pname .flr { transform: rotate(0deg); @@ -1005,8 +1031,9 @@ textarea { /* segment power wrapper */ .sbs { - padding: 1px 0 1px 20px; + /*padding: 1px 0 1px 20px;*/ display: var(--sgp); + width: 100%; } .pname { @@ -1048,11 +1075,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 */ @@ -1066,7 +1091,6 @@ textarea { .pwr { color: var(--c-6); - transform: translate(1px, 1px); cursor: pointer; } @@ -1081,18 +1105,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; @@ -1234,7 +1253,6 @@ TD .checkmark, TD .radiomark { .lbl-s { display: inline-block; - /* margin: 10px 4px 0 0; */ margin-top: 6px; font-size: 13px; width: 48%; @@ -1284,10 +1302,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; @@ -1405,7 +1419,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; } @@ -1534,18 +1548,17 @@ TD .checkmark, TD .radiomark { #sliders .sliderbubble { display: none; } - .sliderwrap { - width: calc(100% - 28px); + #sliders .sliderwrap, .sbs .sliderwrap { + width: calc(100% - 42px); + } + #sliders .slider { + padding-right: 0; } #sliders .sliderwrap { 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 4df0ad8e..0cf48d6e 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -67,12 +67,12 @@ - +

Brightness

-
+
@@ -88,7 +88,7 @@
-
+
@@ -199,7 +199,7 @@
- +
@@ -363,7 +363,7 @@
- +

@@ -379,8 +379,8 @@
-