From 243032c5e47518c0f06dcefbb1e44fa81018c62d Mon Sep 17 00:00:00 2001 From: lodesmets Date: Sat, 24 Apr 2021 11:25:41 +0200 Subject: [PATCH 1/3] Added digital clock overlay --- wled00/cfg.cpp | 3 ++ wled00/data/settings_time.htm | 12 ++++++ wled00/html_settings.h | 33 +++++++++-------- wled00/overlay.cpp | 70 +++++++++++++++++++++++++++++++++++ wled00/set.cpp | 3 ++ wled00/wled.h | 5 ++- wled00/xml.cpp | 2 + 7 files changed, 112 insertions(+), 16 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fcfecaae2f..9398dd3ba7 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -320,6 +320,9 @@ void deserializeConfig() { CJSON(analogClock5MinuteMarks, ol[F("o5m")]); CJSON(analogClockSecondsTrail, ol[F("osec")]); + CJSON(digitalLedPerSegment, ol[F("lps")]); + CJSON(digitalColon, ol[F("dc")]); + //timed macro rules JsonObject tm = doc[F("timers")]; JsonObject cntdwn = tm[F("cntdwn")]; diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index f8a0d9f113..fdb232b389 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -27,6 +27,8 @@ gId("cac").style.display="none"; gId("coc").style.display="block"; gId("ccc").style.display="none"; + gId("cdc").style.display="none"; + if (gId("ca").selected) { gId("cac").style.display="block"; @@ -36,6 +38,11 @@ gId("coc").style.display="none"; gId("ccc").style.display="block"; } + if (gId("cd").selected) + { + gId("coc").style.display="none"; + gId("cdc").style.display="block"; + } if (gId("cn").selected) { gId("coc").style.display="none"; @@ -126,6 +133,7 @@

Clock

+
First LED: Last LED:
@@ -138,6 +146,10 @@

Clock

Cronixie Display:
Cronixie Backlight:
+
+ LED per segment:
+ Colon between hours and minutes:
+ Countdown Mode:
Countdown Goal:
Year: 20 Month: Day:
diff --git a/wled00/html_settings.h b/wled00/html_settings.h index 036a626fde..0c5dd7fd06 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -304,7 +304,7 @@ type="submit">Save)====="; // Autogenerated from wled00/data/settings_time.htm, do not edit!! const char PAGE_settings_time[] PROGMEM = R"=====( Time Settings + + +

Starship monitoring dashboard

+ (or rather just a WLED frame rate tester lol)

+ IP:
+ Time per effect: s
+ Effects to test: + + + + +
+ Extra JSON:
+ +
+ LEDs: -, Seg: -, Bri: -
+ FPS min: -, max: -, avg: -

+
+

+ + + + + \ No newline at end of file diff --git a/wled00/data/jsontest.htm b/tools/json_test.htm similarity index 100% rename from wled00/data/jsontest.htm rename to tools/json_test.htm diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 9717589da3..696d33f3c2 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -9,419 +9,523 @@ */ #pragma once #include "wled.h" -#include "Animated_Staircase_config.h" -#define USERMOD_ID_ANIMATED_STAIRCASE 1011 - -/* Initial configuration (available in API and stored in flash) */ -bool enabled = true; // Enable this usermod -unsigned long segment_delay_ms = 150; // Time between switching each segment -unsigned long on_time_ms = 5 * 1000; // The time for the light to stay on -#ifndef TOP_PIR_PIN -unsigned int topMaxTimeUs = 1749; // default echo timout, top -#endif -#ifndef BOTTOM_PIR_PIN -unsigned int bottomMaxTimeUs = 1749; // default echo timout, bottom -#endif - -// Time between checking of the sensors -const int scanDelay = 50; class Animated_Staircase : public Usermod { - private: - // Lights on or off. - // Flipping this will start a transition. - bool on = false; - - // Swipe direction for current transition -#define SWIPE_UP true -#define SWIPE_DOWN false - bool swipe = SWIPE_UP; - - // Indicates which Sensor was seen last (to determine - // the direction when swiping off) -#define LOWER false -#define UPPER true - bool lastSensor = LOWER; - - // Time of the last transition action - unsigned long lastTime = 0; - - // Time of the last sensor check - unsigned long lastScanTime = 0; - - // Last time the lights were switched on or off - unsigned long lastSwitchTime = 0; - - // segment id between onIndex and offIndex are on. - // controll the swipe by setting/moving these indices around. - // onIndex must be less than or equal to offIndex - byte onIndex = 0; - byte offIndex = 0; - - // The maximum number of configured segments. - // Dynamically updated based on user configuration. - byte maxSegmentId = 1; - byte mainSegmentId = 0; - - bool saveState = false; - - // These values are used by the API to read the - // last sensor state, or trigger a sensor - // through the API - bool topSensorRead = false; - bool topSensorWrite = false; - bool bottomSensorRead = false; - bool bottomSensorWrite = false; - - void updateSegments() { - mainSegmentId = strip.getMainSegmentId(); - WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); - WS2812FX::Segment* segments = strip.getSegments(); - for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { - if (!segments->isActive()) { - maxSegmentId = i - 1; - break; + private: + + /* configuration (available in API and stored in flash) */ + bool enabled = false; // Enable this usermod + unsigned long segment_delay_ms = 150; // Time between switching each segment + unsigned long on_time_ms = 30000; // The time for the light to stay on + int8_t topPIRorTriggerPin = -1; // disabled + int8_t bottomPIRorTriggerPin = -1; // disabled + int8_t topEchoPin = -1; // disabled + int8_t bottomEchoPin = -1; // disabled + bool useUSSensorTop = false; // using PIR or UltraSound sensor? + bool useUSSensorBottom = false; // using PIR or UltraSound sensor? + unsigned int topMaxDist = 50; // default maximum measured distance in cm, top + unsigned int bottomMaxDist = 50; // default maximum measured distance in cm, bottom + + /* runtime variables */ + bool initDone = false; + + // Time between checking of the sensors + const unsigned int scanDelay = 100; + + // Lights on or off. + // Flipping this will start a transition. + bool on = false; + + // Swipe direction for current transition + #define SWIPE_UP true + #define SWIPE_DOWN false + bool swipe = SWIPE_UP; + + // Indicates which Sensor was seen last (to determine + // the direction when swiping off) + #define LOWER false + #define UPPER true + bool lastSensor = LOWER; + + // Time of the last transition action + unsigned long lastTime = 0; + + // Time of the last sensor check + unsigned long lastScanTime = 0; + + // Last time the lights were switched on or off + unsigned long lastSwitchTime = 0; + + // segment id between onIndex and offIndex are on. + // controll the swipe by setting/moving these indices around. + // onIndex must be less than or equal to offIndex + byte onIndex = 0; + byte offIndex = 0; + + // The maximum number of configured segments. + // Dynamically updated based on user configuration. + byte maxSegmentId = 1; + byte mainSegmentId = 0; + + // These values are used by the API to read the + // last sensor state, or trigger a sensor + // through the API + bool topSensorRead = false; + bool topSensorWrite = false; + bool bottomSensorRead = false; + bool bottomSensorWrite = false; + bool topSensorState = false; + bool bottomSensorState = false; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _segmentDelay[]; + static const char _onTime[]; + static const char _useTopUltrasoundSensor[]; + static const char _topPIRorTrigger_pin[]; + static const char _topEcho_pin[]; + static const char _useBottomUltrasoundSensor[]; + static const char _bottomPIRorTrigger_pin[]; + static const char _bottomEcho_pin[]; + static const char _topEchoCm[]; + static const char _bottomEchoCm[]; + + void publishMqtt(bool bottom, const char* state) + { + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[64]; + sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); + mqtt->publish(subuf, 0, false, state); } + } + + void updateSegments() { + mainSegmentId = strip.getMainSegmentId(); + WS2812FX::Segment* segments = strip.getSegments(); + for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { + if (!segments->isActive()) { + maxSegmentId = i - 1; + break; + } - if (i >= onIndex && i < offIndex) { - segments->setOption(SEG_OPTION_ON, 1, 1); + if (i >= onIndex && i < offIndex) { + segments->setOption(SEG_OPTION_ON, 1, 1); - // We may need to copy mode and colors from segment 0 to make sure - // changes are propagated even when the config is changed during a wipe - // segments->mode = mainsegment.mode; - // segments->colors[0] = mainsegment.colors[0]; - } else { - segments->setOption(SEG_OPTION_ON, 0, 1); + // We may need to copy mode and colors from segment 0 to make sure + // changes are propagated even when the config is changed during a wipe + // segments->mode = mainsegment.mode; + // segments->colors[0] = mainsegment.colors[0]; + } else { + segments->setOption(SEG_OPTION_ON, 0, 1); + } + // Always mark segments as "transitional", we are animating the staircase + segments->setOption(SEG_OPTION_TRANSITIONAL, 1, 1); } - // Always mark segments as "transitional", we are animating the staircase - segments->setOption(SEG_OPTION_TRANSITIONAL, 1, 1); + colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); } - colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); - } - - /* - * Detects if an object is within ultrasound range. - * signalPin: The pin where the pulse is sent - * echoPin: The pin where the echo is received - * maxTimeUs: Detection timeout in microseconds. If an echo is - * received within this time, an object is detected - * and the function will return true. - * - * The speed of sound is 343 meters per second at 20 degress Celcius. - * Since the sound has to travel back and forth, the detection - * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. - * - * For practical reasons, here are some useful distances: - * - * Distance = maxtime - * 5 cm = 292 uS - * 10 cm = 583 uS - * 20 cm = 1166 uS - * 30 cm = 1749 uS - * 50 cm = 2915 uS - * 100 cm = 5831 uS - */ - bool ultrasoundRead(uint8_t signalPin, - uint8_t echoPin, - unsigned int maxTimeUs) { - digitalWrite(signalPin, HIGH); - delayMicroseconds(10); - digitalWrite(signalPin, LOW); - return pulseIn(echoPin, HIGH, maxTimeUs) > 0; - } - - void checkSensors() { - if ((millis() - lastScanTime) > scanDelay) { - lastScanTime = millis(); - -#ifdef BOTTOM_PIR_PIN - bottomSensorRead = bottomSensorWrite || (digitalRead(BOTTOM_PIR_PIN) == HIGH); -#else - bottomSensorRead = bottomSensorWrite || ultrasoundRead(BOTTOM_TRIGGER_PIN, BOTTOM_ECHO_PIN, bottomMaxTimeUs); -#endif - -#ifdef TOP_PIR_PIN - topSensorRead = topSensorWrite || (digitalRead(TOP_PIR_PIN) == HIGH); -#else - topSensorRead = topSensorWrite || ultrasoundRead(TOP_TRIGGER_PIN, TOP_ECHO_PIN, topMaxTimeUs); -#endif - - // Values read, reset the flags for next API call - topSensorWrite = false; - bottomSensorWrite = false; - - if (topSensorRead != bottomSensorRead) { - lastSwitchTime = millis(); - if (on) { - lastSensor = topSensorRead; - } else { - // If the bottom sensor triggered, we need to swipe up, ON - swipe = bottomSensorRead; + /* + * Detects if an object is within ultrasound range. + * signalPin: The pin where the pulse is sent + * echoPin: The pin where the echo is received + * maxTimeUs: Detection timeout in microseconds. If an echo is + * received within this time, an object is detected + * and the function will return true. + * + * The speed of sound is 343 meters per second at 20 degress Celcius. + * Since the sound has to travel back and forth, the detection + * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. + * + * For practical reasons, here are some useful distances: + * + * Distance = maxtime + * 5 cm = 292 uS + * 10 cm = 583 uS + * 20 cm = 1166 uS + * 30 cm = 1749 uS + * 50 cm = 2915 uS + * 100 cm = 5831 uS + */ + bool ultrasoundRead(int8_t signalPin, int8_t echoPin, unsigned int maxTimeUs) { + if (signalPin<0 || echoPin<0) return false; + digitalWrite(signalPin, LOW); + delayMicroseconds(2); + digitalWrite(signalPin, HIGH); + delayMicroseconds(10); + digitalWrite(signalPin, LOW); + return pulseIn(echoPin, HIGH, maxTimeUs) > 0; + } - if (swipe) { - Serial.println("ON -> Swipe up."); - } else { - Serial.println("ON -> Swipe down."); - } + bool checkSensors() { + bool sensorChanged = false; + + if ((millis() - lastScanTime) > scanDelay) { + lastScanTime = millis(); + + bottomSensorRead = bottomSensorWrite || + (!useUSSensorBottom ? + (bottomPIRorTriggerPin<0 ? false : digitalRead(bottomPIRorTriggerPin)) : + ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxDist*59) // cm to us + ); + topSensorRead = topSensorWrite || + (!useUSSensorTop ? + (topPIRorTriggerPin<0 ? false : digitalRead(topPIRorTriggerPin)) : + ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxDist*59) // cm to us + ); + + if (bottomSensorRead != bottomSensorState) { + bottomSensorState = bottomSensorRead; // change previous state + sensorChanged = true; + publishMqtt(true, bottomSensorState ? "on" : "off"); + DEBUG_PRINTLN(F("Bottom sensor changed.")); + } + + if (topSensorRead != topSensorState) { + topSensorState = topSensorRead; // change previous state + sensorChanged = true; + publishMqtt(false, topSensorState ? "on" : "off"); + DEBUG_PRINTLN(F("Top sensor changed.")); + } - if (onIndex == offIndex) { - // Position the indices for a correct on-swipe - if (swipe == SWIPE_UP) { - onIndex = mainSegmentId; - } else { - onIndex = maxSegmentId+1; + // Values read, reset the flags for next API call + topSensorWrite = false; + bottomSensorWrite = false; + + if (topSensorRead != bottomSensorRead) { + lastSwitchTime = millis(); + + if (on) { + lastSensor = topSensorRead; + } else { + // If the bottom sensor triggered, we need to swipe up, ON + swipe = bottomSensorRead; + + DEBUG_PRINT(F("ON -> Swipe ")); + DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); + + if (onIndex == offIndex) { + // Position the indices for a correct on-swipe + if (swipe == SWIPE_UP) { + onIndex = mainSegmentId; + } else { + onIndex = maxSegmentId+1; + } + offIndex = onIndex; } - offIndex = onIndex; + on = true; } - on = true; } } + return sensorChanged; } - } - void autoPowerOff() { - if (on && ((millis() - lastSwitchTime) > on_time_ms)) { - // Swipe OFF in the direction of the last sensor detection - swipe = lastSensor; - on = false; + void autoPowerOff() { + if (on && ((millis() - lastSwitchTime) > on_time_ms)) { + // if sensors are still on, do nothing + if (bottomSensorState || topSensorState) return; - if (swipe) { - Serial.println("OFF -> Swipe up."); - } else { - Serial.println("OFF -> Swipe down."); + // Swipe OFF in the direction of the last sensor detection + swipe = lastSensor; + on = false; + + DEBUG_PRINT(F("OFF -> Swipe ")); + DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); } } - } - void updateSwipe() { - if ((millis() - lastTime) > segment_delay_ms) { - lastTime = millis(); + void updateSwipe() { + if ((millis() - lastTime) > segment_delay_ms) { + lastTime = millis(); - byte oldOnIndex = onIndex; - byte oldOffIndex = offIndex; - - if (on) { - // Turn on all segments - onIndex = MAX(mainSegmentId, onIndex - 1); - offIndex = MIN(maxSegmentId + 1, offIndex + 1); - } else { - if (swipe == SWIPE_UP) { - onIndex = MIN(offIndex, onIndex + 1); + if (on) { + // Turn on all segments + onIndex = MAX(mainSegmentId, onIndex - 1); + offIndex = MIN(maxSegmentId + 1, offIndex + 1); } else { - offIndex = MAX(onIndex, offIndex - 1); + if (swipe == SWIPE_UP) { + onIndex = MIN(offIndex, onIndex + 1); + } else { + offIndex = MAX(onIndex, offIndex - 1); + } } + updateSegments(); } + } - updateSegments(); + // send sesnor values to JSON API + void writeSensorsToJson(JsonObject& staircase) { + staircase[F("top-sensor")] = topSensorRead; + staircase[F("bottom-sensor")] = bottomSensorRead; } - } - void writeSettingsToJson(JsonObject& root) { - JsonObject staircase = root["staircase"]; - if (staircase.isNull()) { - staircase = root.createNestedObject("staircase"); + // allow overrides from JSON API + void readSensorsFromJson(JsonObject& staircase) { + bottomSensorWrite = bottomSensorState || (staircase[F("bottom-sensor")].as()); + topSensorWrite = topSensorState || (staircase[F("top-sensor")].as()); } - staircase["enabled"] = enabled; - staircase["segment-delay-ms"] = segment_delay_ms; - staircase["on-time-s"] = on_time_ms / 1000; - -#ifdef TOP_TRIGGER_PIN - staircase["top-echo-us"] = topMaxTimeUs; -#endif -#ifdef BOTTOM_TRIGGER_PIN - staircase["bottom-echo-us"] = bottomMaxTimeUs; -#endif - } - - void writeSensorsToJson(JsonObject& root) { - JsonObject staircase = root["staircase"]; - if (staircase.isNull()) { - staircase = root.createNestedObject("staircase"); + + void enable(bool enable) { + if (enable) { + DEBUG_PRINTLN(F("Animated Staircase enabled.")); + DEBUG_PRINT(F("Delay between steps: ")); + DEBUG_PRINT(segment_delay_ms); + DEBUG_PRINT(F(" milliseconds.\nStairs switch off after: ")); + DEBUG_PRINT(on_time_ms / 1000); + DEBUG_PRINTLN(F(" seconds.")); + + if (!useUSSensorBottom) + pinMode(bottomPIRorTriggerPin, INPUT_PULLUP); + else { + pinMode(bottomPIRorTriggerPin, OUTPUT); + pinMode(bottomEchoPin, INPUT); + } + + if (!useUSSensorTop) + pinMode(topPIRorTriggerPin, INPUT_PULLUP); + else { + pinMode(topPIRorTriggerPin, OUTPUT); + pinMode(topEchoPin, INPUT); + } + } else { + // Restore segment options + WS2812FX::Segment* segments = strip.getSegments(); + for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { + if (!segments->isActive()) { + maxSegmentId = i - 1; + break; + } + segments->setOption(SEG_OPTION_ON, 1, 1); + } + colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); + DEBUG_PRINTLN(F("Animated Staircase disabled.")); + } + enabled = enable; } - staircase["top-sensor"] = topSensorRead; - staircase["bottom-sensor"] = bottomSensorRead; - } - - bool readSettingsFromJson(JsonObject& root) { - JsonObject staircase = root["staircase"]; - bool changed = false; - - bool shouldEnable = staircase["enabled"] | enabled; - if (shouldEnable != enabled) { - enable(shouldEnable); - changed = true; + + public: + void setup() { + // allocate pins + if (topPIRorTriggerPin >= 0) { + if (!pinManager.allocatePin(topPIRorTriggerPin,useUSSensorTop)) + topPIRorTriggerPin = -1; + } + if (topEchoPin >= 0) { + if (!pinManager.allocatePin(topEchoPin,false)) + topEchoPin = -1; + } + if (bottomPIRorTriggerPin >= 0) { + if (!pinManager.allocatePin(bottomPIRorTriggerPin,useUSSensorBottom)) + bottomPIRorTriggerPin = -1; + } + if (bottomEchoPin >= 0) { + if (!pinManager.allocatePin(bottomEchoPin,false)) + bottomEchoPin = -1; + } + enable(enabled); + initDone = true; } - unsigned long c_segment_delay_ms = staircase["segment-delay-ms"] | segment_delay_ms; - if (c_segment_delay_ms != segment_delay_ms) { - segment_delay_ms = c_segment_delay_ms; - changed = true; + void loop() { + if (!enabled || strip.isUpdating()) return; + checkSensors(); + autoPowerOff(); + updateSwipe(); } - unsigned long c_on_time_ms = (staircase["on-time-s"] | (on_time_ms / 1000)) * 1000; - if (c_on_time_ms != on_time_ms) { - on_time_ms = c_on_time_ms; - changed = true; + uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } + + /** + * handling of MQTT message + * topic only contains stripped topic (part after /wled/MAC) + * topic should look like: /swipe with amessage of [up|down] + */ + bool onMqttMessage(char* topic, char* payload) { + if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/swipe"), 6) == 0) { + String action = payload; + if (action == "up") { + bottomSensorWrite = true; + return true; + } else if (action == "down") { + topSensorWrite = true; + return true; + } else if (action == "on") { + enable(true); + return true; + } else if (action == "off") { + enable(false); + return true; + } + } + return false; } -#ifdef TOP_TRIGGER_PIN - unsigned int c_topMaxTimeUs = staircase["top-echo-us"] | topMaxTimeUs; - if (c_topMaxTimeUs != topMaxTimeUs) { - topMaxTimeUs = c_topMaxTimeUs; - changed = true; + /** + * subscribe to MQTT topic for controlling usermod + */ + void onMqttConnect(bool sessionPresent) { + //(re)subscribe to required topics + char subuf[64]; + if (mqttDeviceTopic[0] != 0) { + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/swipe")); + mqtt->subscribe(subuf, 0); + } } -#endif -#ifdef BOTTOM_TRIGGER_PIN - unsigned int c_bottomMaxTimeUs = staircase["bottom-echo-us"] | bottomMaxTimeUs; - if (c_bottomMaxTimeUs != bottomMaxTimeUs) { - bottomMaxTimeUs = c_bottomMaxTimeUs; - changed = true; + + void addToJsonState(JsonObject& root) { + JsonObject staircase = root[FPSTR(_name)]; + if (staircase.isNull()) { + staircase = root.createNestedObject(FPSTR(_name)); + } + writeSensorsToJson(staircase); + DEBUG_PRINTLN(F("Staircase sensor state exposed in API.")); } -#endif - - return changed; - } - - void readSensorsFromJson(JsonObject& root) { - JsonObject staircase = root["staircase"]; - bottomSensorWrite = bottomSensorRead || (staircase["bottom-sensor"].as()); - topSensorWrite = topSensorRead || (staircase["top-sensor"].as()); - } - - void enable(bool enable) { - if (enable) { - Serial.println("Animated Staircase enabled."); - Serial.print("Delay between steps: "); - Serial.print(segment_delay_ms, DEC); - Serial.print(" milliseconds.\nStairs switch off after: "); - Serial.print(on_time_ms / 1000, DEC); - Serial.println(" seconds."); - -#ifdef BOTTOM_PIR_PIN - pinMode(BOTTOM_PIR_PIN, INPUT); -#else - pinMode(BOTTOM_TRIGGER_PIN, OUTPUT); - pinMode(BOTTOM_ECHO_PIN, INPUT); -#endif - -#ifdef TOP_PIR_PIN - pinMode(TOP_PIR_PIN, INPUT); -#else - pinMode(TOP_TRIGGER_PIN, OUTPUT); - pinMode(TOP_ECHO_PIN, INPUT); -#endif - } else { - // Restore segment options - WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); - WS2812FX::Segment* segments = strip.getSegments(); - for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { - if (!segments->isActive()) { - maxSegmentId = i - 1; - break; + + /* + * Reads configuration settings from the json API. + * See void addToJsonState(JsonObject& root) + */ + void readFromJsonState(JsonObject& root) { + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject staircase = root[FPSTR(_name)]; + if (!staircase.isNull()) { + if (staircase[FPSTR(_enabled)].is()) { + enabled = staircase[FPSTR(_enabled)].as(); + } else { + String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on + enabled = (bool)(str!="off"); // off is guaranteed to be present } - segments->setOption(SEG_OPTION_ON, 1, 1); + readSensorsFromJson(staircase); + DEBUG_PRINTLN(F("Staircase sensor state read from API.")); } - colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); - Serial.println("Animated Staircase disabled."); } - enabled = enable; - } - - public: - void setup() { enable(enabled); } - void loop() { - // Write changed settings from to flash (see readFromJsonState()) - if (saveState) { - serializeConfig(); - saveState = false; + /* + * Writes the configuration to internal flash memory. + */ + void addToConfig(JsonObject& root) { + JsonObject staircase = root[FPSTR(_name)]; + if (staircase.isNull()) { + staircase = root.createNestedObject(FPSTR(_name)); + } + staircase[FPSTR(_enabled)] = enabled; + staircase[FPSTR(_segmentDelay)] = segment_delay_ms; + staircase[FPSTR(_onTime)] = on_time_ms / 1000; + staircase[FPSTR(_useTopUltrasoundSensor)] = useUSSensorTop; + staircase[FPSTR(_topPIRorTrigger_pin)] = topPIRorTriggerPin; + staircase[FPSTR(_topEcho_pin)] = useUSSensorTop ? topEchoPin : -1; + staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom; + staircase[FPSTR(_bottomPIRorTrigger_pin)] = bottomPIRorTriggerPin; + staircase[FPSTR(_bottomEcho_pin)] = useUSSensorBottom ? bottomEchoPin : -1; + staircase[FPSTR(_topEchoCm)] = topMaxDist; + staircase[FPSTR(_bottomEchoCm)] = bottomMaxDist; + DEBUG_PRINTLN(F("Staircase config saved.")); } - if (!enabled) { - return; - } + /* + * Reads the configuration to internal flash memory before setup() is called. + */ + void readFromConfig(JsonObject& root) { + bool oldUseUSSensorTop = useUSSensorTop; + bool oldUseUSSensorBottom = useUSSensorBottom; + int8_t oldTopAPin = topPIRorTriggerPin; + int8_t oldTopBPin = topEchoPin; + int8_t oldBottomAPin = bottomPIRorTriggerPin; + int8_t oldBottomBPin = bottomEchoPin; + + JsonObject staircase = root[FPSTR(_name)]; + if (!staircase.isNull()) { + if (staircase[FPSTR(_enabled)].is()) { + enabled = staircase[FPSTR(_enabled)].as(); + } else { + String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on + enabled = (bool)(str!="off"); // off is guaranteed to be present + } + segment_delay_ms = min(10000,max(10,staircase[FPSTR(_segmentDelay)].as())); // max delay 10s + on_time_ms = min(900,max(10,staircase[FPSTR(_onTime)].as())) * 1000; // min 10s, max 15min + + if (staircase[FPSTR(_useTopUltrasoundSensor)].is()) { + useUSSensorTop = staircase[FPSTR(_useTopUltrasoundSensor)].as(); + } else { + String str = staircase[FPSTR(_useTopUltrasoundSensor)]; // checkbox -> off or on + useUSSensorTop = (bool)(str!="off"); // off is guaranteed to be present + } + + topPIRorTriggerPin = min(39,max(-1,staircase[FPSTR(_topPIRorTrigger_pin)].as())); + topEchoPin = min(39,max(-1,staircase[FPSTR(_topEcho_pin)].as())); - checkSensors(); - autoPowerOff(); - updateSwipe(); - - } - - uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } - - /* - * Shows configuration settings to the json API. This object looks like: - * - * "staircase" : { - * "enabled" : true - * "segment-delay-ms" : 150, - * "on-time-s" : 5 - * } - * - */ - void addToJsonState(JsonObject& root) { - writeSettingsToJson(root); - writeSensorsToJson(root); - Serial.println("Staircase config exposed in API."); - } - - /* - * Reads configuration settings from the json API. - * See void addToJsonState(JsonObject& root) - */ - void readFromJsonState(JsonObject& root) { - // The call to serializeConfig() must be done in the main loop, - // so we set a flag to signal the main loop to save state. - saveState = readSettingsFromJson(root); - readSensorsFromJson(root); - Serial.println("Staircase config read from API."); - } - - /* - * Writes the configuration to internal flash memory. - */ - void addToConfig(JsonObject& root) { - writeSettingsToJson(root); - Serial.println("Staircase config saved."); - } - - /* - * Reads the configuration to internal flash memory before setup() is called. - */ - void readFromConfig(JsonObject& root) { - readSettingsFromJson(root); - Serial.println("Staircase config loaded."); - } - - /* - * Shows the delay between steps and power-off time in the "info" - * tab of the web-UI. - */ - void addToJsonInfo(JsonObject& root) { - JsonObject staircase = root["u"]; - if (staircase.isNull()) { - staircase = root.createNestedObject("u"); + if (staircase[FPSTR(_useBottomUltrasoundSensor)].is()) { + useUSSensorBottom = staircase[FPSTR(_useBottomUltrasoundSensor)].as(); + } else { + String str = staircase[FPSTR(_useBottomUltrasoundSensor)]; // checkbox -> off or on + useUSSensorBottom = (bool)(str!="off"); // off is guaranteed to be present + } + bottomPIRorTriggerPin = min(39,max(-1,staircase[FPSTR(_bottomPIRorTrigger_pin)].as())); + bottomEchoPin = min(39,max(-1,staircase[FPSTR(_bottomEcho_pin)].as())); + topMaxDist = min(150,max(30,staircase[FPSTR(_topEchoCm)].as())); // max distnace ~1.5m (a lag of 9ms may be expected) + bottomMaxDist = min(150,max(30,staircase[FPSTR(_bottomEchoCm)].as())); // max distance ~1.5m (a lag of 9ms may be expected) + } else { + DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + } + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F("Staircase config loaded.")); + } else { + // changing paramters from settings page + DEBUG_PRINTLN(F("Staircase config (re)loaded.")); + bool changed = false; + if ((oldUseUSSensorTop != useUSSensorTop) || + (oldUseUSSensorBottom != useUSSensorBottom) || + (oldTopAPin != topPIRorTriggerPin) || + (oldTopBPin != topEchoPin) || + (oldBottomAPin != bottomPIRorTriggerPin) || + (oldBottomBPin != bottomEchoPin)) { + changed = true; + pinManager.deallocatePin(oldTopAPin); + pinManager.deallocatePin(oldTopBPin); + pinManager.deallocatePin(oldBottomAPin); + pinManager.deallocatePin(oldBottomBPin); + } + if (changed) setup(); + } } - if (enabled) { - JsonArray usermodEnabled = - staircase.createNestedArray("Staircase enabled"); // name - usermodEnabled.add("yes"); // value - - JsonArray segmentDelay = - staircase.createNestedArray("Delay between stairs"); // name - segmentDelay.add(segment_delay_ms); // value - segmentDelay.add(" milliseconds"); // unit - - JsonArray onTime = - staircase.createNestedArray("Power-off stairs after"); // name - onTime.add(on_time_ms / 1000); // value - onTime.add(" seconds"); // unit - } else { - JsonArray usermodEnabled = - staircase.createNestedArray("Staircase enabled"); // name - usermodEnabled.add("no"); // value + /* + * Shows the delay between steps and power-off time in the "info" + * tab of the web-UI. + */ + void addToJsonInfo(JsonObject& root) { + JsonObject staircase = root["u"]; + if (staircase.isNull()) { + staircase = root.createNestedObject("u"); + } + + JsonArray usermodEnabled = staircase.createNestedArray(F("Staircase")); // name + String btn = F(""); + usermodEnabled.add(btn); // value } - } -}; \ No newline at end of file +}; + +// strings to reduce flash memory usage (used more than twice) +const char Animated_Staircase::_name[] PROGMEM = "staircase"; +const char Animated_Staircase::_enabled[] PROGMEM = "enabled"; +const char Animated_Staircase::_segmentDelay[] PROGMEM = "segment-delay-ms"; +const char Animated_Staircase::_onTime[] PROGMEM = "on-time-s"; +const char Animated_Staircase::_useTopUltrasoundSensor[] PROGMEM = "useTopUltrasoundSensor"; +const char Animated_Staircase::_topPIRorTrigger_pin[] PROGMEM = "topPIRorTrigger_pin"; +const char Animated_Staircase::_topEcho_pin[] PROGMEM = "topEcho_pin"; +const char Animated_Staircase::_useBottomUltrasoundSensor[] PROGMEM = "useBottomUltrasoundSensor"; +const char Animated_Staircase::_bottomPIRorTrigger_pin[] PROGMEM = "bottomPIRorTrigger_pin"; +const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEcho_pin"; +const char Animated_Staircase::_topEchoCm[] PROGMEM = "top-dist-cm"; +const char Animated_Staircase::_bottomEchoCm[] PROGMEM = "bottom-dist-cm"; diff --git a/usermods/Animated_Staircase/Animated_Staircase_config.h b/usermods/Animated_Staircase/Animated_Staircase_config.h deleted file mode 100644 index 9805074771..0000000000 --- a/usermods/Animated_Staircase/Animated_Staircase_config.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Animated_Staircase compiletime confguration. - * - * Please see README.md on how to change this file. - */ - -// Please change the pin numbering below to match your board. -#define TOP_PIR_PIN D5 -#define BOTTOM_PIR_PIN D6 - -// Or uncumment and a pir and use an ultrasound HC-SR04 sensor, -// see README.md for details -#ifndef TOP_PIR_PIN -#define TOP_TRIGGER_PIN D2 -#define TOP_ECHO_PIN D3 -#endif - -#ifndef BOTTOM_PIR_PIN -#define BOTTOM_TRIGGER_PIN D4 -#define BOTTOM_ECHO_PIN D5 -#endif \ No newline at end of file diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 1ee6c28cd3..2641e67632 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -20,44 +20,10 @@ Edit `usermods_list.cpp`: 2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file 3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. -Edit `Animated_Staircase_config.h`: -1. Open `usermods/Animated_Staircase/Animated_Staircase_config.h` -2. To use PIR sensors, change these lines to match your setup: - Using D7 and D6 pin notation as used on several boards: - - ```cpp - #define TOP_PIR_PIN D7 - #define BOTTOM_PIR_PIN D6 - ``` - - Or using GPIO numbering for pins 25 and 26: - ```cpp - #define TOP_PIR_PIN 26 - #define BOTTOM_PIR_PIN 25 - ``` - - To use Ultrasonic HC-SR04 sensors instead of (one of the) PIR sensors, - uncomment one of the PIR sensor lines and adjust the pin numbers for the - connected Ultrasonic sensor. In the example below we use an Ultrasonic - sensor at the bottom of the stairs: - - ```cpp - #define TOP_PIR_PIN 32 - //#define BOTTOM_PIR_PIN D6 /* This PIR sensor is disabled */ - - #ifndef TOP_PIR_PIN - #define TOP_SIGNAL_PIN D2 - #define TOP_ECHO_PIN D3 - #endif - - #ifndef BOTTOM_PIR_PIN /* If the bottom PIR is disabled, */ - #define BOTTOM_SIGNAL_PIN 25 /* This Ultrasonic sensor is used */ - #define BOTTOM_ECHO_PIN 26 - #endif - ``` - -After these modifications, compile and upload your WLED binary to your board -and check the WLED info page to see if this usermod is enabled. +You can configure usermod using Usermods settings page. +Please enter GPIO pins for PIR sensors or ultrasonic sensor (trigger and echo). +If you use PIR sensor enter -1 for echo pin. +Maximum distance for ultrasonic sensor can be configured as a time needed for echo (see below). ## Hardware installation 1. Stick the LED strip under each step of the stairs. @@ -90,9 +56,6 @@ or remove them and put everything on one line. | Setting | Description | Default | |------------------|---------------------------------------------------------------|---------| | enabled | Enable or disable the usermod | true | -| segment-delay-ms | Delay (milliseconds) between switching on/off each step | 150 | -| on-time-s | Time (seconds) the stairs stay lit after last detection | 5 | -| bottom-echo-us | Detection range of ultrasonic sensor | 1749 | | bottom-sensor | Manually trigger a down to up animation via API | false | | top-sensor | Manually trigger an up to down animation via API | false | @@ -106,8 +69,6 @@ The staircase settings and sensor states are inside the WLED status element: "state": { "staircase": { "enabled": true, - "segment-delay-ms": 150, - "on-time-s": 5, "bottom-sensor": false, "tops-ensor": false }, @@ -128,58 +89,16 @@ curl -X POST -H "Content-Type: application/json" \ To enable the usermod again, use `"enabled":true`. -### Changing animation parameters -To change the delay between the steps to (for example) 100 milliseconds and the on-time to -10 seconds: +Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. -```bash -curl -X POST -H "Content-Type: application/json" \ - -d '{"staircase":{"segment-delay-ms":100,"on-time-s":10}}' \ - xxx.xxx.xxx.xxx/json/state -``` +### Changing animation parameters and detection range of the ultrasonic HC-SR04 sensor +Using _Usermod_ Settings page you can define different usermod parameters, includng sensor pins, delay between segment activation and so on. -### Changing detection range of the ultrasonic HC-SR04 sensor -When an ultrasonic sensor is enabled in `Animated_Staircase_config.h`, you'll see a -`bottom-echo-us` setting appear in the json api: - -```json -{ - "state": { - "staircase": { - "enabled": true, - "segment-delay-ms": 150, - "on-time-s": 5, - "bottom-echo-us": 1749 - }, -} -``` - -If the HC-SR04 sensor detects an echo within 1749 microseconds (corresponding to ~30 cm -detection range from the sensor), it will trigger switching on the staircase. This setting -can be changed through the API with an HTTP POST: - -```bash -curl -X POST -H "Content-Type: application/json" \ - -d '{"staircase":{"bottom-echo-us":1166}}' \ - xxx.xxx.xxx.xxx/json/state -``` - -Calculating the detection range can be performed as follows: The speed of sound is 343m/s at 20 -degrees Centigrade. Since the sound has to travel back and forth, the detection range for the -sensor in cm is (0.0343 * maxTimeUs) / 2. To get you started, please find delays and distances below: - -| Distance | Detection time | -|---------:|----------------:| -| 5 cm | 292 uS | -| 10 cm | 583 uS | -| 20 cm | 1166 uS | -| 30 cm | 1749 uS | -| 50 cm | 2915 uS | -| 100 cm | 5831 uS | +When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. **Please note:** that using an HC-SR04 sensor, particularly when detecting echos at longer distances creates delays in the WLED software, and _might_ introduce timing hickups in your animations or -a less responsive web interface. It is therefore advised to keep the detection time as short as possible. +a less responsive web interface. It is therefore advised to keep the detection distance as short as possible. ### Animation triggering through the API Instead of stairs activation by one of the sensors, you can also trigger the animation through @@ -198,6 +117,15 @@ curl -X POST -H "Content-Type: application/json" \ -d '{"staircase":{"top-sensor":true}}' \ xxx.xxx.xxx.xxx/json/state ``` +**MQTT** +You can publish a message with either `up` or `down` on topic `/swipe` to trigger animation. +You can also use `on` or `off` for enabling or disabling usermod. Have fun with this usermod.
www.rolfje.com + +Modifications @blazoncek + +## Change log +2021-04 +* Adaptation for runtime configuration. \ No newline at end of file diff --git a/usermods/EleksTube_IPS/ChipSelect.h b/usermods/EleksTube_IPS/ChipSelect.h new file mode 100644 index 0000000000..c58b285496 --- /dev/null +++ b/usermods/EleksTube_IPS/ChipSelect.h @@ -0,0 +1,70 @@ +#ifndef CHIP_SELECT_H +#define CHIP_SELECT_H + +#include "Hardware.h" + +/* + * `digit`s are as defined in Hardware.h, 0 == seconds ones, 5 == hours tens. + */ + +class ChipSelect { +private: + uint8_t digits_map; + const uint8_t all_on = 0x3F; + const uint8_t all_off = 0x00; +public: + ChipSelect() : digits_map(all_off) {} + + void update() { + // Documented in README.md. Q7 and Q6 are unused. Q5 is Seconds Ones, Q0 is Hours Tens. + // Q7 is the first bit written, Q0 is the last. So we push two dummy bits, then start with + // Seconds Ones and end with Hours Tens. + // CS is Active Low, but digits_map is 1 for enable, 0 for disable. So we bit-wise NOT first. + + uint8_t to_shift = (~digits_map) << 2; + + digitalWrite(CSSR_LATCH_PIN, LOW); + shiftOut(CSSR_DATA_PIN, CSSR_CLOCK_PIN, LSBFIRST, to_shift); + digitalWrite(CSSR_LATCH_PIN, HIGH); + } + + void begin() + { + pinMode(CSSR_LATCH_PIN, OUTPUT); + pinMode(CSSR_DATA_PIN, OUTPUT); + pinMode(CSSR_CLOCK_PIN, OUTPUT); + + digitalWrite(CSSR_DATA_PIN, LOW); + digitalWrite(CSSR_CLOCK_PIN, LOW); + digitalWrite(CSSR_LATCH_PIN, LOW); + update(); + } + + // These speak the indexes defined in Hardware.h. + // So 0 is disabled, 1 is enabled (even though CS is active low, this gets mapped.) + // So bit 0 (LSB), is index 0, is SECONDS_ONES + // Translation to what the 74HC595 uses is done in update() + void setDigitMap(uint8_t map, bool update_=true) { digits_map = map; if (update_) update(); } + uint8_t getDigitMap() { return digits_map; } + + // Helper functions + // Sets just the one digit by digit number + void setDigit(uint8_t digit, bool update_=true) { setDigitMap(0x01 << digit, update_); } + void setAll(bool update_=true) { setDigitMap(all_on, update_); } + void clear(bool update_=true) { setDigitMap(all_off, update_); } + void setSecondsOnes() { setDigit(SECONDS_ONES); } + void setSecondsTens() { setDigit(SECONDS_TENS); } + void setMinutesOnes() { setDigit(MINUTES_ONES); } + void setMinutesTens() { setDigit(MINUTES_TENS); } + void setHoursOnes() { setDigit(HOURS_ONES); } + void setHoursTens() { setDigit(HOURS_TENS); } + bool isSecondsOnes() { return (digits_map&SECONDS_ONES_MAP > 0); } + bool isSecondsTens() { return (digits_map&SECONDS_TENS_MAP > 0); } + bool isMinutesOnes() { return (digits_map&MINUTES_ONES_MAP > 0); } + bool isMinutesTens() { return (digits_map&MINUTES_TENS_MAP > 0); } + bool isHoursOnes() { return (digits_map&HOURS_ONES_MAP > 0); } + bool isHoursTens() { return (digits_map&HOURS_TENS_MAP > 0); } +}; + + +#endif // CHIP_SELECT_H diff --git a/usermods/EleksTube_IPS/Hardware.h b/usermods/EleksTube_IPS/Hardware.h new file mode 100644 index 0000000000..e4f7930536 --- /dev/null +++ b/usermods/EleksTube_IPS/Hardware.h @@ -0,0 +1,52 @@ +/* + * Define the hardware for the EleksTube IPS clock. Mostly pin definitions + */ +#ifndef ELEKSTUBEHAX_HARDWARE_H +#define ELEKSTUBEHAX_HARDWARE_H + +#include +#include // for HIGH and LOW + +// Common indexing scheme, used to identify the digit +#define SECONDS_ONES (0) +#define SECONDS_TENS (1) +#define MINUTES_ONES (2) +#define MINUTES_TENS (3) +#define HOURS_ONES (4) +#define HOURS_TENS (5) +#define NUM_DIGITS (6) + +#define SECONDS_ONES_MAP (0x01 << SECONDS_ONES) +#define SECONDS_TENS_MAP (0x01 << SECONDS_TENS) +#define MINUTES_ONES_MAP (0x01 << MINUTES_ONES) +#define MINUTES_TENS_MAP (0x01 << MINUTES_TENS) +#define HOURS_ONES_MAP (0x01 << HOURS_ONES) +#define HOURS_TENS_MAP (0x01 << HOURS_TENS) + +// WS2812 (or compatible) LEDs on the back of the display modules. +#define BACKLIGHTS_PIN (12) + +// Buttons, active low, externally pulled up (with actual resistors!) +#define BUTTON_LEFT_PIN (33) +#define BUTTON_MODE_PIN (32) +#define BUTTON_RIGHT_PIN (35) +#define BUTTON_POWER_PIN (34) + +// I2C to DS3231 RTC. +#define RTC_SCL_PIN (22) +#define RTC_SDA_PIN (21) + +// Chip Select shift register, to select the display +#define CSSR_DATA_PIN (14) +#define CSSR_CLOCK_PIN (16) +#define CSSR_LATCH_PIN (17) + +// SPI to displays +// DEFINED IN User_Setup.h +// Look for: TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, and TFT_RST + +// Power for all TFT displays are grounded through a MOSFET so they can all be turned off. +// Active HIGH. +#define TFT_ENABLE_PIN (27) + +#endif // ELEKSTUBEHAX_HARDWARE_H diff --git a/usermods/EleksTube_IPS/TFTs.h b/usermods/EleksTube_IPS/TFTs.h new file mode 100644 index 0000000000..675129dce4 --- /dev/null +++ b/usermods/EleksTube_IPS/TFTs.h @@ -0,0 +1,218 @@ +#ifndef TFTS_H +#define TFTS_H + +#include "wled.h" +#include + +#include +#include "Hardware.h" +#include "ChipSelect.h" + +class TFTs : public TFT_eSPI { +private: + uint8_t digits[NUM_DIGITS]; + + // These read 16- and 32-bit types from the SD card file. + // BMP data is stored little-endian, Arduino is little-endian too. + // May need to reverse subscript order if porting elsewhere. + + uint16_t read16(fs::File &f) { + uint16_t result; + ((uint8_t *)&result)[0] = f.read(); // LSB + ((uint8_t *)&result)[1] = f.read(); // MSB + return result; + } + + uint32_t read32(fs::File &f) { + uint32_t result; + ((uint8_t *)&result)[0] = f.read(); // LSB + ((uint8_t *)&result)[1] = f.read(); + ((uint8_t *)&result)[2] = f.read(); + ((uint8_t *)&result)[3] = f.read(); // MSB + return result; + } + + uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH]; + + + // These BMP functions are stolen directly from the TFT_SPIFFS_BMP example in the TFT_eSPI library. + // Unfortunately, they aren't part of the library itself, so I had to copy them. + // I've modified drawBmp to buffer the whole image at once instead of doing it line-by-line. + + //// BEGIN STOLEN CODE + + // Draw directly from file stored in RGB565 format + bool drawBin(const char *filename) { + fs::File bmpFS; + + + // Open requested file on SD card + bmpFS = WLED_FS.open(filename, "r"); + + if (!bmpFS) + { + Serial.print(F("File not found: ")); + Serial.println(filename); + return(false); + } + + size_t sz = bmpFS.size(); + if (sz <= 64800) + { + bool oldSwapBytes = getSwapBytes(); + setSwapBytes(true); + + int16_t h = sz / (135 * 2); + + //draw img that is shorter than 240pix into the center + int16_t y = (height() - h) /2; + + bmpFS.read((uint8_t *) output_buffer,sz); + + if (!realtimeMode || realtimeOverride) strip.service(); + + pushImage(0, y, 135, h, (uint16_t *)output_buffer); + + setSwapBytes(oldSwapBytes); + } + + bmpFS.close(); + + return(true); + } + + bool drawBmp(const char *filename) { + fs::File bmpFS; + + // Open requested file on SD card + bmpFS = WLED_FS.open(filename, "r"); + + if (!bmpFS) + { + Serial.print(F("File not found: ")); + Serial.println(filename); + return(false); + } + + uint32_t seekOffset; + int16_t w, h, row; + uint8_t r, g, b; + + uint16_t magic = read16(bmpFS); + if (magic == 0xFFFF) { + Serial.println(F("BMP not found!")); + bmpFS.close(); + return(false); + } + + if (magic != 0x4D42) { + Serial.print(F("File not a BMP. Magic: ")); + Serial.println(magic); + bmpFS.close(); + return(false); + } + + read32(bmpFS); + read32(bmpFS); + seekOffset = read32(bmpFS); + read32(bmpFS); + w = read32(bmpFS); + h = read32(bmpFS); + + if ((read16(bmpFS) != 1) || (read16(bmpFS) != 24) || (read32(bmpFS) != 0)) { + Serial.println(F("BMP format not recognized.")); + bmpFS.close(); + return(false); + } + + //draw img that is shorter than 240pix into the center + int16_t y = (height() - h) /2; + + bool oldSwapBytes = getSwapBytes(); + setSwapBytes(true); + bmpFS.seek(seekOffset); + + uint16_t padding = (4 - ((w * 3) & 3)) & 3; + uint8_t lineBuffer[w * 3 + padding]; + + uint8_t serviceStrip = (!realtimeMode || realtimeOverride) ? 7 : 0; + // row is decremented as the BMP image is drawn bottom up + for (row = h-1; row >= 0; row--) { + if ((row & 0b00000111) == serviceStrip) strip.service(); //still refresh backlight to mitigate stutter every few rows + bmpFS.read(lineBuffer, sizeof(lineBuffer)); + uint8_t* bptr = lineBuffer; + + // Convert 24 to 16 bit colours while copying to output buffer. + for (uint16_t col = 0; col < w; col++) + { + b = *bptr++; + g = *bptr++; + r = *bptr++; + output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + } + } + + pushImage(0, y, w, h, (uint16_t *)output_buffer); + setSwapBytes(oldSwapBytes); + + bmpFS.close(); + return(true); + } + +public: + TFTs() : TFT_eSPI(), chip_select() + { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; } + + // no == Do not send to TFT. yes == Send to TFT if changed. force == Send to TFT. + enum show_t { no, yes, force }; + // A digit of 0xFF means blank the screen. + const static uint8_t blanked = 255; + + void begin() { + pinMode(TFT_ENABLE_PIN, OUTPUT); + digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot + + // Start with all displays selected. + chip_select.begin(); + chip_select.setAll(); + + // Initialize the super class. + init(); + } + + void showDigit(uint8_t digit) { + chip_select.setDigit(digit); + + if (digits[digit] == blanked) { + fillScreen(TFT_BLACK); + } + else { + // Filenames are no bigger than "255.bmp\0" + char file_name[10]; + sprintf(file_name, "/%d.bmp", digits[digit]); + if (WLED_FS.exists(file_name)) { + drawBmp(file_name); + } else { + sprintf(file_name, "/%d.bin", digits[digit]); + drawBin(file_name); + } + } + } + + void setDigit(uint8_t digit, uint8_t value, show_t show=yes) { + uint8_t old_value = digits[digit]; + digits[digit] = value; + + if (show != no && (old_value != value || show == force)) { + showDigit(digit); + } + } + uint8_t getDigit(uint8_t digit) { return digits[digit]; } + + void showAllDigits() { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit); } + + // Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly. + ChipSelect chip_select; +}; + +#endif // TFTS_H diff --git a/usermods/EleksTube_IPS/User_Setup.h b/usermods/EleksTube_IPS/User_Setup.h new file mode 100644 index 0000000000..b4b2edab02 --- /dev/null +++ b/usermods/EleksTube_IPS/User_Setup.h @@ -0,0 +1,47 @@ +/* + * This is intended to over-ride `User_Setup.h` that comes with the TFT_eSPI library. + * I hate having to modify the library code. + */ + +// ST7789 135 x 240 display with no chip select line + +#define ST7789_DRIVER // Configure all registers + +#define TFT_WIDTH 135 +#define TFT_HEIGHT 240 + +#define CGRAM_OFFSET // Library will add offsets required + +//#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue +//#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red + +//#define TFT_INVERSION_ON +//#define TFT_INVERSION_OFF + +// EleksTube IPS +#define TFT_SDA_READ // Read and write on the MOSI/SDA pin, no separate MISO pin +#define TFT_MOSI 23 +#define TFT_SCLK 18 +//#define TFT_CS -1 // Not connected +#define TFT_DC 25 // Data Command, aka Register Select or RS +#define TFT_RST 26 // Connect reset to ensure display initialises + +#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH +//#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters +//#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters +//#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm +//#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. +//#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. +//#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT +//#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts + +//#define SMOOTH_FONT + + +//#define SPI_FREQUENCY 27000000 +#define SPI_FREQUENCY 40000000 + +/* + * To make the Library not over-write all this: + */ +#define USER_SETUP_LOADED diff --git a/usermods/EleksTube_IPS/readme.md b/usermods/EleksTube_IPS/readme.md new file mode 100644 index 0000000000..a08d69d2eb --- /dev/null +++ b/usermods/EleksTube_IPS/readme.md @@ -0,0 +1,31 @@ +# EleksTube IPS Clock usermod + +This usermod allows WLED to run on the EleksTube IPS clock. +It enables running all WLED effects on the background SK6812 lighting, while displaying digit bitmaps on the 6 IPS screens. +Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith! + +Supported: +- Display with custom bitmaps or raw RGB565 images (.bin) from filesystem +- Background lighting +- Power button +- RTC (with RTC usermod) +- Standard WLED time features (NTP, DST, timezones) + +Not supported: +- 3 navigation buttons, on-device setup + +Your images must be exactly 135 pixels wide and 1-240 pixels high. + +## Installation + +Compile and upload to clock using the `elekstube_ips` PlatformIO environment +Once uploaded (the clock can be flashed like any ESP32 module), go to `[WLED-IP]/edit` and upload the 0-9.bin files from [here](https://github.com/Aircoookie/NixieThemes/tree/master/themes/RealisticNixie/bin). +You can find more clockfaces in the [NixieThemes](https://github.com/Aircoookie/NixieThemes/) repo. +Use LED pin 12, relay pin 27 and button pin 34. + +## Use of RGB565 images + +Binary 16-bit per pixel RGB565 format `.bin` images are now supported. This has the benefit of only using 2/3rds of the file size a `.bmp` has. +The drawback is that this format cannot be handled by common image programs and that an extra conversion step is needed. +You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`) +Thank you to @RedNax67 for adding .bin support. \ No newline at end of file diff --git a/usermods/EleksTube_IPS/usermod_elekstube_ips.h b/usermods/EleksTube_IPS/usermod_elekstube_ips.h new file mode 100644 index 0000000000..f2ce8eb08f --- /dev/null +++ b/usermods/EleksTube_IPS/usermod_elekstube_ips.h @@ -0,0 +1,60 @@ +#pragma once +#include "TFTs.h" +#include "wled.h" + +//Large parts of the code are from https://github.com/SmittyHalibut/EleksTubeHAX + +class ElekstubeIPSUsermod : public Usermod { + private: + TFTs tfts; + void updateClockDisplay(TFTs::show_t show=TFTs::yes) { + bool set[6] = {false}; + for (uint8_t i = 0; i<6; i++) { + char c = cronixieDisplay[i]; + if (c >= '0' && c <= '9') { + tfts.setDigit(5-i, c - '0', show); set[i] = true; + } else if (c >= 'A' && c <= 'G') { + tfts.setDigit(5-i, c - 'A' + 10, show); set[i] = true; //10.bmp to 16.bmp static display + } else if (c == '-' || c == '_' || c == ' ') { + tfts.setDigit(5-i, 255, show); set[i] = true; //blank + } else { + set[i] = false; //display HHMMSS time + } + } + uint8_t hr = hour(localTime); + uint8_t hrTens = hr/10; + uint8_t mi = minute(localTime); + uint8_t mittens = mi/10; + uint8_t s = second(localTime); + uint8_t sTens = s/10; + if (!set[0]) tfts.setDigit(HOURS_TENS, hrTens, show); + if (!set[1]) tfts.setDigit(HOURS_ONES, hr - hrTens*10, show); + if (!set[2]) tfts.setDigit(MINUTES_TENS, mittens, show); + if (!set[3]) tfts.setDigit(MINUTES_ONES, mi - mittens*10, show); + if (!set[4]) tfts.setDigit(SECONDS_TENS, sTens, show); + if (!set[5]) tfts.setDigit(SECONDS_ONES, s - sTens*10, show); + } + unsigned long lastTime = 0; + public: + + void setup() { + tfts.begin(); + tfts.fillScreen(TFT_BLACK); + + for (int8_t i = 5; i >= 0; i--) { + tfts.setDigit(i, 255, TFTs::force); //turn all off + } + } + + void loop() { + if (toki.isTick()) { + updateLocalTime(); + updateClockDisplay(); + } + } + + uint16_t getId() + { + return USERMOD_ID_ELEKSTUBE_IPS; + } +}; \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index 3d00b50542..9b17c3ad02 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -9,28 +9,13 @@ The LED strip is switched [using a relay](https://github.com/Aircoookie/WLED/wik ## Webinterface -The info page in the web interface shows the items below - -- the state of the sensor. By clicking on the state the sensor can be deactivated/activated. Changes persist after a reboot. -**I recommend to deactivate the sensor before an OTA update and activate it again afterwards**. -- the remaining time of the off timer. - -## JSON API - -The usermod supports the following state changes: - -| JSON key | Value range | Description | -|------------|-------------|---------------------------------| -| PIRenabled | bool | Deactivdate/activate the sensor | -| PIRoffSec | 60 to 43200 | Off timer seconds | - - Changes also persist after a reboot. +The info page in the web interface shows the remaining time of the off timer. ## Sensor connection My setup uses an HC-SR501 sensor, a HC-SR505 should also work. -The usermod uses GPIO13 (D1 mini pin D7) for the sensor signal. +The usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal but can be changed in the Usermod settings page. [This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor. Use the potentiometers on the sensor to set the time-delay to the minimum and the sensitivity to about half, or slightly above. @@ -76,9 +61,10 @@ void registerUsermods() ## API to enable/disable the PIR sensor from outside. For example from another usermod. -The class provides the static method `PIRsensorSwitch* PIRsensorSwitch::GetInstance()` to get a pointer to the usermod object. +To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. -To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. +When the PIR sensor state changes an MQTT message is broadcasted with topic `wled/deviceMAC/motion` and message `on` or `off`. +Usermod can also be configured to just send MQTT message and not change WLED state using settings page as well as responding to motion only during nighttime (assuming NTP and lattitude/longitude are set to determine sunrise/sunset times). ### There are two options to get access to the usermod instance: @@ -98,12 +84,19 @@ class MyUsermod : public Usermod { //... void togglePIRSensor() { - if (PIRsensorSwitch::GetInstance() != nullptr) { - PIRsensorSwitch::GetInstance()->EnablePIRsensor(!PIRsensorSwitch::GetInstance()->PIRsensorEnabled()); + #ifdef USERMOD_PIR_SENSOR_SWITCH + PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH); + if (PIRsensor != nullptr) { + PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); } + #endif } //... }; ``` Have fun - @gegu + +## Change log +2021-04 +* Adaptation for runtime configuration. \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 421528bfe6..9b7ccff20e 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -2,6 +2,15 @@ #include "wled.h" +#ifndef PIR_SENSOR_PIN + // compatible with QuinLED-Dig-Uno + #ifdef ARDUINO_ARCH_ESP32 + #define PIR_SENSOR_PIN 23 // Q4 + #else //ESP8266 boards + #define PIR_SENSOR_PIN 13 // Q4 (D7 on D1 mini) + #endif +#endif + /* * This usermod handles PIR sensor states. * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. @@ -30,104 +39,128 @@ class PIRsensorSwitch : public Usermod /** * constructor */ - PIRsensorSwitch() - { - // set static instance pointer - PIRsensorSwitchInstance(this); - } + PIRsensorSwitch() {} /** * desctructor */ - ~PIRsensorSwitch() - { - PIRsensorSwitchInstance(nullptr, true); - ; - } - - /** - * return the instance pointer of the class - */ - static PIRsensorSwitch *GetInstance() { return PIRsensorSwitchInstance(); } + ~PIRsensorSwitch() {} /** * Enable/Disable the PIR sensor */ - void EnablePIRsensor(bool enable) { m_PIRenabled = enable; } + void EnablePIRsensor(bool en) { enabled = en; } /** * Get PIR sensor enabled/disabled state */ - bool PIRsensorEnabled() { return m_PIRenabled; } + bool PIRsensorEnabled() { return enabled; } private: // PIR sensor pin - const uint8_t PIRsensorPin = 13; // D7 on D1 mini + int8_t PIRsensorPin = PIR_SENSOR_PIN; // notification mode for colorUpdated() const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE // delay before switch off after the sensor state goes LOW - uint32_t m_switchOffDelay = 600000; + uint32_t m_switchOffDelay = 600000; // 10min // off timer start time uint32_t m_offTimerStart = 0; // current PIR sensor pin state - byte m_PIRsensorPinState = LOW; - // PIR sensor enabled - ISR attached - bool m_PIRenabled = true; - // state if serializeConfig() should be called - bool m_updateConfig = false; - - /** - * return or change if new PIR sensor state is available - */ - static volatile bool newPIRsensorState(bool changeState = false, bool newState = false); + byte sensorPinState = LOW; + // PIR sensor enabled + bool enabled = true; + // status of initialisation + bool initDone = false; + // on and off presets + uint8_t m_onPreset = 0; + uint8_t m_offPreset = 0; + // flag to indicate that PIR sensor should activate WLED during nighttime only + bool m_nightTimeOnly = false; + // flag to send MQTT message only (assuming it is enabled) + bool m_mqttOnly = false; + + unsigned long lastLoop = 0; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _switchOffDelay[]; + static const char _enabled[]; + static const char _onPreset[]; + static const char _offPreset[]; + static const char _nightTime[]; + static const char _mqttOnly[]; /** - * PIR sensor state has changed + * check if it is daytime + * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime */ - static void IRAM_ATTR ISR_PIRstateChange(); - - /** - * Set/get instance pointer - */ - static PIRsensorSwitch *PIRsensorSwitchInstance(PIRsensorSwitch *pInstance = nullptr, bool bRemoveInstance = false); + bool isDayTime() { + bool isDayTime = false; + updateLocalTime(); + uint8_t hr = hour(localTime); + uint8_t mi = minute(localTime); + + if (sunrise && sunset) { + if (hour(sunrise)
hr) { + isDayTime = true; + } else { + if (hour(sunrise)==hr && minute(sunrise)mi) { + isDayTime = true; + } + } + } + return isDayTime; + } /** * switch strip on/off */ void switchStrip(bool switchOn) { - if (switchOn && bri == 0) - { + if (switchOn && m_onPreset) { + applyPreset(m_onPreset); + } else if (!switchOn && m_offPreset) { + applyPreset(m_offPreset); + } else if (switchOn && bri == 0) { bri = briLast; colorUpdated(NotifyUpdateMode); - } - else if (!switchOn && bri != 0) - { + } else if (!switchOn && bri != 0) { briLast = bri; bri = 0; colorUpdated(NotifyUpdateMode); } } + void publishMqtt(const char* state) + { + //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); + } + } + /** * Read and update PIR sensor state. * Initilize/reset switch off timer */ bool updatePIRsensorState() { - if (newPIRsensorState()) - { - m_PIRsensorPinState = digitalRead(PIRsensorPin); + bool pinState = digitalRead(PIRsensorPin); + if (pinState != sensorPinState) { + sensorPinState = pinState; // change previous state - if (m_PIRsensorPinState == HIGH) - { + if (sensorPinState == HIGH) { m_offTimerStart = 0; - switchStrip(true); - } - else if (bri != 0) - { + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); + publishMqtt("on"); + } else /*if (bri != 0)*/ { // start switch off timer m_offTimerStart = millis(); } - newPIRsensorState(true, false); return true; } return false; @@ -140,9 +173,10 @@ class PIRsensorSwitch : public Usermod { if (m_offTimerStart > 0 && millis() - m_offTimerStart > m_switchOffDelay) { - if (m_PIRenabled == true) + if (enabled == true) { - switchStrip(false); + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(false); + publishMqtt("off"); } m_offTimerStart = 0; return true; @@ -159,13 +193,19 @@ class PIRsensorSwitch : public Usermod */ void setup() { - // PIR Sensor mode INPUT_PULLUP - pinMode(PIRsensorPin, INPUT_PULLUP); - if (m_PIRenabled) - { - // assign interrupt function and set CHANGE mode - attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); + // pin retrieved from cfg.json (readFromConfig()) prior to running setup() + if (!pinManager.allocatePin(PIRsensorPin,false)) { + PIRsensorPin = -1; // allocation failed + enabled = false; + DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); + } else { + // PIR Sensor mode INPUT_PULLUP + pinMode(PIRsensorPin, INPUT_PULLUP); + if (enabled) { + sensorPinState = digitalRead(PIRsensorPin); + } } + initDone = true; } /** @@ -181,14 +221,13 @@ class PIRsensorSwitch : public Usermod */ void loop() { - if (!updatePIRsensorState()) - { + // only check sensors 10x/s + unsigned long now = millis(); + if (now - lastLoop < 100) return; + lastLoop = now; + + if (!updatePIRsensorState()) { handleOffTimer(); - if (m_updateConfig) - { - serializeConfig(); - m_updateConfig = false; - } } } @@ -199,132 +238,167 @@ class PIRsensorSwitch : public Usermod */ void addToJsonInfo(JsonObject &root) { - //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object - // the value contains a button to toggle the sensor enabled/disabled JsonObject user = root["u"]; - if (user.isNull()) - user = root.createNestedObject("u"); + if (user.isNull()) user = root.createNestedObject("u"); - JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name - String uiDomString = ""; - infoArr.add(uiDomString); //value - - //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object - uiDomString = "⏲ switch off timer\ -after min"; - infoArr = user.createNestedArray(uiDomString); //name - - // off timer - if (m_offTimerStart > 0) - { - uiDomString = ""; - unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; - if (offSeconds >= 3600) - { - uiDomString += (offSeconds / 3600); - uiDomString += " hours "; - offSeconds %= 3600; - } - if (offSeconds >= 60) - { - uiDomString += (offSeconds / 60); - offSeconds %= 60; - } - else if (uiDomString.length() > 0) - { - uiDomString += 0; - } - if (uiDomString.length() > 0) + // off timer + String uiDomString = F("PIR "); + JsonArray infoArr = user.createNestedArray(uiDomString); // timer value + if (m_offTimerStart > 0) { - uiDomString += " min "; + uiDomString = ""; + unsigned int offSeconds = (m_switchOffDelay - (millis() - m_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")); } - uiDomString += (offSeconds); - infoArr.add(uiDomString + " sec"); - } - else - { - infoArr.add("inactive"); + } else { + String uiDomString = F("PIR sensor"); + JsonArray infoArr = user.createNestedArray(uiDomString); + infoArr.add(F("disabled")); } } /** * 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 - * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. - * Add "PIRoffSec" to json state. This can be used to adjust milliseconds. */ +/* void addToJsonState(JsonObject &root) { - root["PIRenabled"] = m_PIRenabled; - root["PIRoffSec"] = (m_switchOffDelay / 1000); } +*/ /** * 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 - * Read "PIRenabled" from json state and switch enable/disable the PIR sensor. - * Read "PIRoffSec" from json state and adjust milliseconds. */ +/* void readFromJsonState(JsonObject &root) { - if (root["PIRoffSec"] != nullptr) - { - m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as()))); - m_updateConfig = true; - } - - if (root["PIRenabled"] != nullptr) - { - if (root["PIRenabled"] && !m_PIRenabled) - { - attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); - newPIRsensorState(true, true); - } - else if (m_PIRenabled) - { - detachInterrupt(PIRsensorPin); - } - m_PIRenabled = root["PIRenabled"]; - m_updateConfig = true; - } } +*/ /** * provide the changeable values */ void addToConfig(JsonObject &root) { - JsonObject top = root.createNestedObject("PIRsensorSwitch"); - top["PIRenabled"] = m_PIRenabled; - top["PIRoffSec"] = m_switchOffDelay; + 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; + DEBUG_PRINTLN(F("PIR config saved.")); } /** * restore the changeable values + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json */ void readFromConfig(JsonObject &root) { - JsonObject top = root["PIRsensorSwitch"]; - m_PIRenabled = (top["PIRenabled"] != nullptr ? top["PIRenabled"] : true); - m_switchOffDelay = top["PIRoffSec"] | m_switchOffDelay; + bool oldEnabled = enabled; + int8_t oldPin = PIRsensorPin; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) return; + + if (top["pin"] != nullptr) { + PIRsensorPin = min(39,max(-1,top["pin"].as())); // check bounds + } + + if (top[FPSTR(_enabled)] != nullptr) { + if (top[FPSTR(_enabled)].is()) { + enabled = top[FPSTR(_enabled)].as(); // reading from cfg.json + } else { + // change from settings page + String str = top[FPSTR(_enabled)]; // checkbox -> off or on + enabled = (bool)(str!="off"); // off is guaranteed to be present + } + } + + if (top[FPSTR(_switchOffDelay)] != nullptr) { + m_switchOffDelay = (top[FPSTR(_switchOffDelay)].as() * 1000); + } + + if (top[FPSTR(_onPreset)] != nullptr) { + m_onPreset = max(0,min(250,top[FPSTR(_onPreset)].as())); + } + + if (top[FPSTR(_offPreset)] != nullptr) { + m_offPreset = max(0,min(250,top[FPSTR(_offPreset)].as())); + } + + if (top[FPSTR(_nightTime)] != nullptr) { + if (top[FPSTR(_nightTime)].is()) { + m_nightTimeOnly = top[FPSTR(_nightTime)].as(); // reading from cfg.json + } else { + // change from settings page + String str = top[FPSTR(_nightTime)]; // checkbox -> off or on + m_nightTimeOnly = (bool)(str!="off"); // off is guaranteed to be present + } + } + + if (top[FPSTR(_mqttOnly)] != nullptr) { + if (top[FPSTR(_mqttOnly)].is()) { + m_mqttOnly = top[FPSTR(_mqttOnly)].as(); // reading from cfg.json + } else { + // change from settings page + String str = top[FPSTR(_mqttOnly)]; // checkbox -> off or on + m_mqttOnly = (bool)(str!="off"); // off is guaranteed to be present + } + } + + if (!initDone) { + // reading config prior to setup() + DEBUG_PRINTLN(F("PIR 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); + if (pinManager.allocatePin(PIRsensorPin,false)) { + pinMode(PIRsensorPin, INPUT_PULLUP); + } else { + // allocation failed + PIRsensorPin = -1; + enabled = false; + } + } + if (enabled) { + sensorPinState = digitalRead(PIRsensorPin); + } + DEBUG_PRINTLN(F("PIR config (re)loaded.")); + } + } } /** @@ -337,30 +411,11 @@ after = prevValue + maxDiff; + } + + uint16_t getLuminance() + { + // http://forum.arduino.cc/index.php?topic=37555.0 + // https://forum.arduino.cc/index.php?topic=185158.0 + float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); + float amps = volts / resistorValue; + float lux = amps * 1000000 * 2.0; + + lastMeasurement = millis(); + getLuminanceComplete = true; + return uint16_t(lux); + } + +public: + void setup() + { + // set pinmode + pinMode(PHOTORESISTOR_PIN, INPUT); + } + + void loop() + { + if (disabled || strip.isUpdating()) + return; + + unsigned long now = millis(); + + // check to see if we are due for taking a measurement + // lastMeasurement will not be updated until the conversion + // is complete the the reading is finished + if (now - lastMeasurement < readingInterval) + { + return; + } + + uint16_t currentLDRValue = getLuminance(); + if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) + { + lastLDRValue = currentLDRValue; + + if (WLED_MQTT_CONNECTED) + { + char subuf[45]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/luminance")); + mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str()); + } + else + { + DEBUG_PRINTLN("Missing MQTT connection. Not publishing data"); + } + } + } + + void addToJsonInfo(JsonObject &root) + { + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + JsonArray lux = user.createNestedArray(F("Luminance")); + + if (!getLuminanceComplete) + { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux.add(F(" sec until read")); + return; + } + + lux.add(lastLDRValue); + lux.add(F(" lux")); + } + + uint16_t getId() + { + return USERMOD_ID_SN_PHOTORESISTOR; + } + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root) + { + // we add JSON object. + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = !disabled; + top[FPSTR(_readInterval)] = readingInterval / 1000; + top[FPSTR(_referenceVoltage)] = referenceVoltage; + top[FPSTR(_resistorValue)] = resistorValue; + top[FPSTR(_adcPrecision)] = adcPrecision; + top[FPSTR(_offset)] = offset; + + DEBUG_PRINTLN(F("Photoresistor config saved.")); + } + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + */ + void readFromConfig(JsonObject &root) + { + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + + if (!top.isNull()) + { + if (top[FPSTR(_enabled)].is()) + { + disabled = !top[FPSTR(_enabled)].as(); + } + else + { + String str = top[FPSTR(_enabled)]; // checkbox -> off or on + disabled = (bool)(str == "off"); // off is guaranteed to be present + }; + + readingInterval = min(120, max(10, top[FPSTR(_readInterval)].as())) * 1000; // convert to ms + referenceVoltage = top[FPSTR(_referenceVoltage)].as(); + resistorValue = top[FPSTR(_resistorValue)].as(); + adcPrecision = top[FPSTR(_adcPrecision)].as(); + offset = top[FPSTR(_offset)].as(); + DEBUG_PRINTLN(F("Photoresistor config (re)loaded.")); + } + else + { + DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + } + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char Usermod_SN_Photoresistor::_name[] PROGMEM = "Photoresistor"; +const char Usermod_SN_Photoresistor::_enabled[] PROGMEM = "enabled"; +const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s"; +const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; +const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; +const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; +const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; \ No newline at end of file diff --git a/usermods/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp new file mode 100644 index 0000000000..649e197392 --- /dev/null +++ b/usermods/SN_Photoresistor/usermods_list.cpp @@ -0,0 +1,14 @@ +#include "wled.h" +/* + * Register your v2 usermods here! + */ +#ifdef USERMOD_SN_PHOTORESISTOR +#include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" +#endif + +void registerUsermods() +{ +#ifdef USERMOD_SN_PHOTORESISTOR + usermods.add(new Usermod_SN_Photoresistor()); +#endif +} \ No newline at end of file diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index 5b7f5b9585..ac2823c3f8 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -3,7 +3,7 @@ Based on the excellent `QuinLED_Dig_Uno_Temp_MQTT` by srg74 and 400killer! This usermod will read from an attached DS18B20 temperature sensor (as available on the QuinLED Dig-Uno) The temperature is displayed both in the Info section of the web UI as well as published to the `/temperature` MQTT topic if enabled. -This usermod will be expanded with support for different sensor types in the future. +This usermod may be expanded with support for different sensor types in the future. If temperature sensor is not detected during boot, this usermod will be disabled. @@ -14,20 +14,21 @@ Copy the example `platformio_override.ini` to the root directory. This file sho ### Define Your Options * `USERMOD_DALLASTEMPERATURE` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_DALLASTEMPERATURE_CELSIUS` - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported -* `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60 seconds * `USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 20 seconds +All parameters can be configured at runtime using Usermods settings page, including pin, selection to display temerature in degrees Celsius or Farenheit mand measurement interval. + ## Project link * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link +* [Srg74-WLED-Wemos-shield](https://github.com/srg74/WLED-wemos-shield) - another great DIY WLED board ### PlatformIO requirements If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:d1_mini_usermod_dallas_temperature_C`. -If you are not using `platformio_override.ini`, you might have to uncomment `DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: +If you are not using `platformio_override.ini`, you might have to uncomment `OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: ```ini # platformio.ini @@ -41,10 +42,7 @@ default_envs = d1_mini ... lib_deps = ... - #For use SSD1306 OLED display uncomment following - U8g2@~2.27.3 - #For Dallas sensor uncomment following 2 lines - DallasTemperature@~3.8.0 + #For Dallas sensor uncomment following line OneWire@~2.3.5 ... ``` @@ -56,3 +54,5 @@ lib_deps = * Do not report low temperatures that indicate an error to mqtt * Disable plugin if temperature sensor not detected * Report the number of seconds until the first read in the info screen instead of sensor error +2021-04 +* Adaptation for runtime configuration. \ No newline at end of file diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index 9019fd175d..3a6bbfc417 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -1,16 +1,15 @@ #pragma once #include "wled.h" +#include "OneWire.h" -#include //DS18B20 - -//Pin defaults for QuinLed Dig-Uno +//Pin defaults for QuinLed Dig-Uno if not overriden #ifndef TEMPERATURE_PIN -#ifdef ARDUINO_ARCH_ESP32 -#define TEMPERATURE_PIN 18 -#else //ESP8266 boards -#define TEMPERATURE_PIN 14 -#endif + #ifdef ARDUINO_ARCH_ESP32 + #define TEMPERATURE_PIN 18 + #else //ESP8266 boards + #define TEMPERATURE_PIN 14 + #endif #endif // the frequency to check temperature, 1 minute @@ -20,19 +19,20 @@ // how many seconds after boot to take first measurement, 20 seconds #ifndef USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT -#define USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT 20000 +#define USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT 20000 #endif -OneWire oneWire(TEMPERATURE_PIN); -DallasTemperature sensor(&oneWire); - class UsermodTemperature : public Usermod { + private: - // The device's unique 64-bit serial code stored in on-board ROM. - // Reading directly from the sensor device address is faster than - // reading from index. When reading by index, DallasTemperature - // must first look up the device address at the specified index. - DeviceAddress sensorDeviceAddress; + + bool initDone = false; + OneWire *oneWire; + // GPIO pin used for sensor (with a default compile-time fallback) + int8_t temperaturePin = TEMPERATURE_PIN; + // measurement unit (true==°C, false==°F) + bool degC = true; + unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; // set last reading as "40 sec before boot", so first reading is taken after 20 sec unsigned long lastMeasurement = UINT32_MAX - (USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT); // last time requestTemperatures was called @@ -42,92 +42,129 @@ class UsermodTemperature : public Usermod { float temperature = -100; // default to -100, DS18B20 only goes down to -50C // indicates requestTemperatures has been called but the sensor measurement is not complete bool waitingForConversion = false; - // flag to indicate we have finished the first getTemperature call + // flag to indicate we have finished the first readTemperature call // allows this library to report to the user how long until the first // measurement - bool getTemperatureComplete = false; + bool readTemperatureComplete = false; // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting // temperature if flashed to a board without a sensor attached bool disabled = false; - void requestTemperatures() { - // there is requestTemperaturesByAddress however it - // appears to do more work, - // TODO: measure exection time difference - sensor.requestTemperatures(); - lastTemperaturesRequest = millis(); - waitingForConversion = true; + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _readInterval[]; + + //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 + int16_t readDallas() { + byte i; + byte data[2]; + int16_t result; // raw data from sensor + oneWire->reset(); + oneWire->write(0xCC); // skip ROM + oneWire->write(0xBE); // read (temperature) from EEPROM + for (i=0; i < 2; i++) data[i] = oneWire->read(); // first 2 bytes contain temperature + for (i=2; i < 8; i++) oneWire->read(); // read unused bytes + result = (data[1]<<8) | data[0]; + result >>= 4; // 9-bit precision accurate to 1°C (/16) + if (data[1]&0x80) result |= 0xF000; // fix negative value + //if (data[0]&0x08) ++result; + oneWire->reset(); + oneWire->write(0xCC); // skip ROM + oneWire->write(0x44,0); // request new temperature reading (without parasite power) + return result; } - void getTemperature() { - if (strip.isUpdating()) return; - #ifdef USERMOD_DALLASTEMPERATURE_CELSIUS - temperature = sensor.getTempC(sensorDeviceAddress); - #else - temperature = sensor.getTempF(sensorDeviceAddress); - #endif + void requestTemperatures() { + readDallas(); + lastTemperaturesRequest = millis(); + waitingForConversion = true; + DEBUG_PRINTLN(F("Requested temperature.")); + } + void readTemperature() { + temperature = readDallas(); lastMeasurement = millis(); waitingForConversion = false; - getTemperatureComplete = true; + readTemperatureComplete = true; + DEBUG_PRINTF("Read temperature %2.1f.\n", temperature); } - public: + bool findSensor() { + DEBUG_PRINTLN(F("Searching for sensor...")); + uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0}; + // find out if we have DS18xxx sensor attached + oneWire->reset_search(); + while (oneWire->search(deviceAddress)) { + if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) { + switch (deviceAddress[0]) { + case 0x10: // DS18S20 + case 0x22: // DS18B20 + case 0x28: // DS1822 + case 0x3B: // DS1825 + case 0x42: // DS28EA00 + DEBUG_PRINTLN(F("Sensor found.")); + return true; + } + } + } + return false; + } + public: void setup() { - sensor.begin(); - - // get the unique 64-bit serial code stored in on-board ROM - // if getAddress returns false, the sensor was not found - disabled = !sensor.getAddress(sensorDeviceAddress, 0); - - if (!disabled) { - DEBUG_PRINTLN(F("Dallas Temperature found")); - // set the resolution for this specific device - sensor.setResolution(sensorDeviceAddress, 9, true); - // do not block waiting for reading - sensor.setWaitForConversion(false); - // allocate pin & prevent other use - if (!pinManager.allocatePin(TEMPERATURE_PIN,false)) - disabled = true; + int retries = 10; + // pin retrieved from cfg.json (readFromConfig()) prior to running setup() + if (!pinManager.allocatePin(temperaturePin,false)) { + temperaturePin = -1; // allocation failed + disabled = true; + DEBUG_PRINTLN(F("Temperature pin allocation failed.")); } else { - DEBUG_PRINTLN(F("Dallas Temperature not found")); + if (!disabled) { + // config says we are enabled + oneWire = new OneWire(temperaturePin); + if (!oneWire->reset()) + disabled = true; // resetting 1-Wire bus yielded an error + else + while ((disabled=!findSensor()) && retries--) delay(25); // try to find sensor + } } + initDone = true; } void loop() { if (disabled || strip.isUpdating()) return; - + unsigned long now = millis(); // check to see if we are due for taking a measurement // lastMeasurement will not be updated until the conversion // is complete the the reading is finished - if (now - lastMeasurement < USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL) return; + if (now - lastMeasurement < readingInterval) return; - // we are due for a measurement, if we are not already waiting + // we are due for a measurement, if we are not already waiting // for a conversion to complete, then make a new request for temps - if (!waitingForConversion) - { + if (!waitingForConversion) { requestTemperatures(); return; } // we were waiting for a conversion to complete, have we waited log enough? - if (now - lastTemperaturesRequest >= 94 /* 93.75ms per the datasheet */) - { - getTemperature(); - + if (now - lastTemperaturesRequest >= 100 /* 93.75ms per the datasheet but can be up to 750ms */) { + readTemperature(); + if (WLED_MQTT_CONNECTED) { - char subuf[45]; + char subuf[64]; strcpy(subuf, mqttDeviceTopic); if (-100 <= temperature) { // dont publish super low temperature as the graph will get messed up // the DallasTemperature library returns -127C or -196.6F when problem // reading the sensor strcat_P(subuf, PSTR("/temperature")); - mqtt->publish(subuf, 0, true, String(temperature).c_str()); + mqtt->publish(subuf, 0, false, String(temperature).c_str()); + strcat_P(subuf, PSTR("_f")); + mqtt->publish(subuf, 0, false, String((float)temperature * 1.8f + 32).c_str()); } else { // publish something else to indicate status? } @@ -135,16 +172,32 @@ class UsermodTemperature : public Usermod { } } + /* + * API calls te enable data exchange between WLED modules + */ + inline float getTemperatureC() { + return (float)temperature; + } + inline float getTemperatureF() { + return (float)temperature * 1.8f + 32; + } + + /* + * 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) { // dont add temperature to info if we are disabled if (disabled) return; - JsonObject user = root[F("u")]; - if (user.isNull()) user = root.createNestedObject(F("u")); + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); - JsonArray temp = user.createNestedArray(F("Temperature")); + JsonArray temp = user.createNestedArray(FPSTR(_name)); + //temp.add(F("Loaded.")); - if (!getTemperatureComplete) { + if (!readTemperatureComplete) { // if we haven't read the sensor yet, let the user know // that we are still waiting for the first measurement temp.add((USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT - millis()) / 1000); @@ -158,12 +211,85 @@ class UsermodTemperature : public Usermod { return; } - temp.add(temperature); - #ifdef USERMOD_DALLASTEMPERATURE_CELSIUS - temp.add(F("°C")); - #else - temp.add(F("°F")); - #endif + temp.add(degC ? temperature : (float)temperature * 1.8f + 32); + if (degC) temp.add(F("°C")); + else temp.add(F("°F")); + } + + /** + * 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) + //{ + //} + + /** + * 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 + * Read "_" from json state and and change settings (i.e. GPIO pin) used. + */ + //void readFromJsonState(JsonObject &root) { + // if (!initDone) return; // prevent crash on boot applyPreset() + //} + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root) { + // we add JSON object: {"Temperature": {"pin": 0, "degC": true}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = !disabled; + top["pin"] = temperaturePin; // usermodparam + top["degC"] = degC; // usermodparam + top[FPSTR(_readInterval)] = readingInterval / 1000; + DEBUG_PRINTLN(F("Temperature config saved.")); + } + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + */ + void readFromConfig(JsonObject &root) { + // we look for JSON object: {"Temperature": {"pin": 0, "degC": true}} + JsonObject top = root[FPSTR(_name)]; + int8_t newTemperaturePin = temperaturePin; + + if (!top.isNull() && top["pin"] != nullptr) { + if (top[FPSTR(_enabled)].is()) { + disabled = !top[FPSTR(_enabled)].as(); + } else { + String str = top[FPSTR(_enabled)]; // checkbox -> off or on + disabled = (bool)(str=="off"); // off is guaranteed to be present + } + newTemperaturePin = min(39,max(-1,top["pin"].as())); + if (top["degC"].is()) { + // reading from cfg.json + degC = top["degC"].as(); + } else { + // new configuration from set.cpp + String str = top["degC"]; // checkbox -> off or on + degC = (bool)(str!="off"); // off is guaranteed to be present + } + readingInterval = min(120,max(10,top[FPSTR(_readInterval)].as())) * 1000; // convert to ms + DEBUG_PRINTLN(F("Temperature config (re)loaded.")); + } else { + DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + } + + if (!initDone) { + // first run: reading from cfg.json + temperaturePin = newTemperaturePin; + } else { + // changing paramters from settings page + if (newTemperaturePin != temperaturePin) { + // deallocate pin and release memory + delete oneWire; + pinManager.deallocatePin(temperaturePin); + temperaturePin = newTemperaturePin; + // initialise + setup(); + } + } } uint16_t getId() @@ -171,3 +297,8 @@ class UsermodTemperature : public Usermod { return USERMOD_ID_TEMPERATURE; } }; + +// strings to reduce flash memory usage (used more than twice) +const char UsermodTemperature::_name[] PROGMEM = "Temperature"; +const char UsermodTemperature::_enabled[] PROGMEM = "enabled"; +const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s"; \ No newline at end of file diff --git a/usermods/UserModv2_SunRiseAndSet/UserMod_SunRiseAndSet.h b/usermods/UserModv2_SunRiseAndSet/UserMod_SunRiseAndSet.h index 62176ce920..ef1bb37ec7 100644 --- a/usermods/UserModv2_SunRiseAndSet/UserMod_SunRiseAndSet.h +++ b/usermods/UserModv2_SunRiseAndSet/UserMod_SunRiseAndSet.h @@ -85,7 +85,7 @@ class UserMod_SunRiseAndSet : public Usermod if (m_pD2D && (999000000L != ntpLastSyncTime)) { // to prevent needing to import all the timezone stuff from other modules, work completely in UTC - time_t timeUTC = now(); + time_t timeUTC = toki.second(); tmElements_t tmNow; breakTime(timeUTC, tmNow); int nCurMinute = tmNow.Minute; diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md new file mode 100644 index 0000000000..a5f47bf5e5 --- /dev/null +++ b/usermods/multi_relay/readme.md @@ -0,0 +1,79 @@ +# Multi Relay + +This usermod-v2 modification allows the connection of multiple relays each with individual delay and on/off mode. + +## HTTP API +All responses are returned as JSON. + +Status Request: `http://[device-ip]/relays` +Switch Command: `http://[device-ip]/relays?switch=1,0,1,1` +The number of numbers behind the switch parameter must correspond to the number of relays. The number 1 switches the relay on. The number 0 switches the relay off. + +Toggle Command: `http://[device-ip]/relays?toggle=1,0,1,1` +The number of numbers behind the parameter switch must correspond to the number of relays. The number 1 causes a toggling of the relay. The number 0 leaves the state of the device. + +Examples +1. 4 relays at all, relay 2 will be toggled: `http://[device-ip]/relays?toggle=0,1,0,0` +2. 3 relays at all, relay 1&3 will be switched on: `http://[device-ip]/relays?switch=1,0,1` + +## MQTT API + +wled/deviceMAC/relay/0/command on|off|toggle +wled/deviceMAC/relay/1/command on|off|toggle + +When relay is switched it will publish a message: + +wled/deviceMAC/relay/0 on|off + + +## Usermod installation + +1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `usermods.add(new MultiRelay());` at the bottom of `usermods_list.cpp`. +or +2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY`in your platformio.ini + +You can override the default maximum number (4) of relays by defining MULTI_RELAY_MAX_RELAYS. + +Example **usermods_list.cpp**: + +```cpp +#include "wled.h" +/* + * Register your v2 usermods here! + * (for v1 usermods using just usermod.cpp, you can ignore this file) + */ + +/* + * Add/uncomment your usermod filename here (and once more below) + * || || || + * \/ \/ \/ + */ +//#include "usermod_v2_example.h" +//#include "usermod_temperature.h" +#include "../usermods/usermod_multi_relay.h" + +void registerUsermods() +{ + /* + * Add your usermod class name here + * || || || + * \/ \/ \/ + */ + //usermods.add(new MyExampleUsermod()); + //usermods.add(new UsermodTemperature()); + usermods.add(new MultiRelay()); + +} +``` + +## Configuration + +Usermod can be configured in Usermods settings page. + +If there is no MultiRelay section, just save current configuration and re-open Usermods settings page. + +Have fun - @blazoncek + +## Change log +2021-04 +* First implementation. \ No newline at end of file diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h new file mode 100644 index 0000000000..42fe39e4c9 --- /dev/null +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -0,0 +1,434 @@ +#pragma once + +#include "wled.h" + +#ifndef MULTI_RELAY_MAX_RELAYS + #define MULTI_RELAY_MAX_RELAYS 4 +#endif + +#define ON true +#define OFF false + +/* + * 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. + */ + + +typedef struct relay_t { + int8_t pin; + bool active; + bool mode; + bool state; + bool external; + uint16_t delay; +} Relay; + + +class MultiRelay : public Usermod { + + private: + // array of relays + Relay _relay[MULTI_RELAY_MAX_RELAYS]; + + // switch timer start time + uint32_t _switchTimerStart = 0; + // old brightness + uint8_t _oldBrightness = 0; + + // usermod enabled + bool enabled = false; // needs to be configured (no default config) + // status of initialisation + bool initDone = false; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _relay_str[]; + static const char _delay_str[]; + static const char _activeHigh[]; + static const char _external[]; + + + void publishMqtt(const char* state, int relay) { + //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, state); + } + } + + /** + * switch off the strip if the delay has elapsed + */ + void handleOffTimer() { + bool activeRelays = false; + for (uint8_t i=0; i 0 && millis() - _switchTimerStart > (_relay[i].delay*1000)) { + if (!_relay[i].external) toggleRelay(i); + _relay[i].active = false; + } + activeRelays = activeRelays || _relay[i].active; + } + if (!activeRelays) _switchTimerStart = 0; + } + + /** + * 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 much arugments 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 mutch arugments 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() { + for (uint8_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(mode ? "on" : "off", relay); + } + + /** + * toggle relay + */ + inline void toggleRelay(uint8_t relay) { + switchRelay(relay, !_relay[relay].state); + } + + uint8_t getActiveRelayCount() { + uint8_t count = 0; + for (uint8_t i=0; i=0) count++; + return count; + } + + //Functions called by WLED + + /** + * 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); + } + } + + /** + * 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 (uint8_t i=0; i=0) _relay[i].active = true; + } + } + + handleOffTimer(); + } + + /** + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + */ + void addToJsonInfo(JsonObject &root) { + if (enabled) { + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(F("Number of relays")); //name + infoArr.add(String(getActiveRelayCount())); + } + } + + /** + * 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) { + } + + /** + * 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) { + } + + /** + * provide the changeable values + */ + void addToConfig(JsonObject &root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + top[FPSTR(_enabled)] = enabled; + for (uint8_t i=0; i()) { + enabled = top[FPSTR(_enabled)].as(); // reading from cfg.json + } else { + // change from settings page + String str = top[FPSTR(_enabled)]; // checkbox -> off or on + enabled = (bool)(str!="off"); // off is guaranteed to be present + } + } + + for (uint8_t i=0; i())); + + if (top[parName+FPSTR(_activeHigh)] != nullptr) { + if (top[parName+FPSTR(_activeHigh)].is()) { + _relay[i].mode = top[parName+FPSTR(_activeHigh)].as(); // reading from cfg.json + } else { + // change from settings page + String str = top[parName+FPSTR(_activeHigh)]; // checkbox -> off or on + _relay[i].mode = (bool)(str!="off"); // off is guaranteed to be present + } + } + + if (top[parName+FPSTR(_external)] != nullptr) { + if (top[parName+FPSTR(_external)].is()) { + _relay[i].external = top[parName+FPSTR(_external)].as(); // reading from cfg.json + } else { + // change from settings page + String str = top[parName+FPSTR(_external)]; // checkbox -> off or on + _relay[i].external = (bool)(str!="off"); // off is guaranteed to be present + } + } + + _relay[i].delay = min(600,max(0,abs(top[parName+FPSTR(_delay_str)].as()))); + } + + if (!initDone) { + // reading config prior to setup() + DEBUG_PRINTLN(F("MultiRelay config loaded.")); + } else { + // deallocate all pins 1st + for (uint8_t i=0; i=0) { + pinManager.deallocatePin(oldPin[i]); + } + // allocate new pins + for (uint8_t i=0; i=0 && pinManager.allocatePin(_relay[i].pin,true)) { + if (!_relay[i].external) switchRelay(i, _relay[i].state = (bool)bri); + } else { + _relay[i].pin = -1; + } + _relay[i].active = false; + } + DEBUG_PRINTLN(F("MultiRelay config (re)loaded.")); + } + } + + /** + * 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; + } +}; + +// 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"; diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md index 5c835c60ef..7cf81085e7 100644 --- a/usermods/usermod_v2_auto_save/readme.md +++ b/usermods/usermod_v2_auto_save/readme.md @@ -29,9 +29,9 @@ This file should be placed in the same directory as `platformio.ini`. ### Define Your Options * `USERMOD_AUTO_SAVE` - define this to have this the Auto Save usermod included wled00\usermods_list.cpp -* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) -* `AUTOSAVE_SETTLE_MS` - Minimum time to wave before auto saving, defaults to 10000 (10s) -* `AUTOSAVE_PRESET_NUM` - Preset number to auto-save to, auto-load at startup from, defaults to 99 +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) + +You can configure auto-save parameters using Usermods settings page. ### PlatformIO requirements @@ -43,3 +43,5 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. 2021-02 * First public release +2021-04 +* Adaptation for runtime configuration. \ No newline at end of file diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index bd7ea6d817..caf542e587 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -2,9 +2,8 @@ #include "wled.h" -// // v2 Usermod to automatically save settings -// to preset number AUTOSAVE_PRESET_NUM after a change to any of +// to configurable preset after a change to any of // // * brightness // * effect speed @@ -12,45 +11,34 @@ // * mode (effect) // * palette // -// but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" +// but it will wait for configurable number of seconds, a "settle" // period in case there are other changes (any change will // extend the "settle" window). // -// It will additionally load preset AUTOSAVE_PRESET_NUM at startup. -// during the first `loop()`. Reasoning below. +// It can be configured to load auto saved preset at startup, +// during the first `loop()`. // // AutoSaveUsermod is standalone, but if FourLineDisplayUsermod // is installed, it will notify the user of the saved changes. -// -// Note: I don't love that WLED doesn't respect the brightness -// of the preset being auto loaded, so the AutoSaveUsermod -// will set the AUTOSAVE_PRESET_NUM preset in the first loop, -// so brightness IS honored. This means WLED will effectively -// ignore Default brightness and Apply N preset at boot when -// the AutoSaveUsermod is installed. - -//How long to wait after settings change to auto-save -#ifndef AUTOSAVE_SETTLE_MS -#define AUTOSAVE_SETTLE_MS 10*1000 -#endif - -//Preset number to save to -#ifndef AUTOSAVE_PRESET_NUM -#define AUTOSAVE_PRESET_NUM 99 -#endif - -// "Auto save MM-DD HH:MM:SS" + +// format: "~ MM-DD HH:MM:SS ~" #define PRESET_NAME_BUFFER_SIZE 25 class AutoSaveUsermod : public Usermod { - private: - // If we've detected the need to auto save, this will - // be non zero. - unsigned long autoSaveAfter = 0; - char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; + private: bool firstLoop = true; + bool initDone = false; + bool enabled = true; + + // configurable parameters + unsigned long autoSaveAfterSec = 15; // 15s by default + uint8_t autoSavePreset = 250; // last possible preset + bool applyAutoSaveOnBoot = false; // do we load auto-saved preset on boot? + + // If we've detected the need to auto save, this will be non zero. + unsigned long autoSaveAfter = 0; uint8_t knownBrightness = 0; uint8_t knownEffectSpeed = 0; @@ -58,35 +46,65 @@ class AutoSaveUsermod : public Usermod { uint8_t knownMode = 0; uint8_t knownPalette = 0; -#ifdef USERMOD_FOUR_LINE_DISLAY + #ifdef USERMOD_FOUR_LINE_DISPLAY FourLineDisplayUsermod* display; -#endif + #endif + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _autoSaveEnabled[]; + static const char _autoSaveAfterSec[]; + static const char _autoSavePreset[]; + static const char _autoSaveApplyOnBoot[]; + + void inline saveSettings() { + char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; + updateLocalTime(); + sprintf_P(presetNameBuffer, + PSTR("~ %02d-%02d %02d:%02d:%02d ~"), + month(localTime), day(localTime), + hour(localTime), minute(localTime), second(localTime)); + savePreset(autoSavePreset, true, presetNameBuffer); + } + + void inline displayOverlay() { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + display->wakeDisplay(); + display->overlay("Settings", "Auto Saved", 1500); + } + #endif + } public: + // gets called once at boot. Do all initialization that doesn't depend on // network here void setup() { -#ifdef USERMOD_FOUR_LINE_DISLAY - // This Usermod has enhanced funcionality if - // FourLineDisplayUsermod is available. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); -#endif + #ifdef USERMOD_FOUR_LINE_DISPLAY + // This Usermod has enhanced funcionality if + // FourLineDisplayUsermod is available. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + #endif + initDone = true; } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void connected() {} - /** + /* * Da loop. */ void loop() { + if (!autoSaveAfterSec || !enabled) return; // setting 0 as autosave seconds disables autosave + unsigned long now = millis(); uint8_t currentMode = strip.getMode(); uint8_t currentPalette = strip.getSegment(0).palette; if (firstLoop) { firstLoop = false; - applyPreset(AUTOSAVE_PRESET_NUM); + if (applyAutoSaveOnBoot) applyPreset(autoSavePreset); knownBrightness = bri; knownEffectSpeed = effectSpeed; knownEffectIntensity = effectIntensity; @@ -95,7 +113,7 @@ class AutoSaveUsermod : public Usermod { return; } - unsigned long wouldAutoSaveAfter = now + AUTOSAVE_SETTLE_MS; + unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000; if (knownBrightness != bri) { knownBrightness = bri; autoSaveAfter = wouldAutoSaveAfter; @@ -121,37 +139,32 @@ class AutoSaveUsermod : public Usermod { } } - void saveSettings() { - updateLocalTime(); - sprintf(presetNameBuffer, - "Auto save %02d-%02d %02d:%02d:%02d", - month(localTime), day(localTime), - hour(localTime), minute(localTime), second(localTime)); - savePreset(AUTOSAVE_PRESET_NUM, true, presetNameBuffer); - } - - void displayOverlay() { -#ifdef USERMOD_FOUR_LINE_DISLAY - if (display != nullptr) { - display->wakeDisplay(); - display->overlay("Settings", "Auto Saved", 1500); - } -#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) { + //JsonObject user = root["u"]; + //if (user.isNull()) user = root.createNestedObject("u"); + //JsonArray data = user.createNestedArray(F("Autosave")); + //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 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) { - } + //void readFromJsonState(JsonObject& root) { + // if (!initDone) return; // prevent crash on boot applyPreset() + //} /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -168,6 +181,13 @@ class AutoSaveUsermod : public Usermod { * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) { + // we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_autoSaveEnabled)] = enabled; + top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam + top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam + top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot; + DEBUG_PRINTLN(F("Autosave config saved.")); } /* @@ -179,7 +199,33 @@ class AutoSaveUsermod : public Usermod { * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) */ void readFromConfig(JsonObject& root) { - } + // we look for JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}} + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + return; + } + + if (top[FPSTR(_autoSaveEnabled)].is()) { + // reading from cfg.json + enabled = top[FPSTR(_autoSaveEnabled)].as(); + } else { + // reading from POST message + String str = top[FPSTR(_autoSaveEnabled)]; // checkbox -> off or on + enabled = (bool)(str!="off"); // off is guaranteed to be present + } + autoSaveAfterSec = min(3600,max(10,top[FPSTR(_autoSaveAfterSec)].as())); + autoSavePreset = min(250,max(100,top[FPSTR(_autoSavePreset)].as())); + if (top[FPSTR(_autoSaveApplyOnBoot)].is()) { + // reading from cfg.json + applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)].as(); + } else { + // reading from POST message + String str = top[FPSTR(_autoSaveApplyOnBoot)]; // checkbox -> off or on + applyAutoSaveOnBoot = (bool)(str!="off"); // off is guaranteed to be present + } + DEBUG_PRINTLN(F("Autosave config (re)loaded.")); + } /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). @@ -188,5 +234,11 @@ class AutoSaveUsermod : public Usermod { uint16_t getId() { return USERMOD_ID_AUTO_SAVE; } - -}; \ No newline at end of file +}; + +// strings to reduce flash memory usage (used more than twice) +const char AutoSaveUsermod::_name[] PROGMEM = "Autosave"; +const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled"; +const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec"; +const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset"; +const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot"; diff --git a/usermods/usermod_v2_four_line_display/readme.md b/usermods/usermod_v2_four_line_display/readme.md index 3198b2be50..367f3d7ac5 100644 --- a/usermods/usermod_v2_four_line_display/readme.md +++ b/usermods/usermod_v2_four_line_display/readme.md @@ -1,4 +1,4 @@ -# Rotary Encoder UI Usermod +# I2C 4 Line Display Usermod First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. @@ -19,13 +19,11 @@ This file should be placed in the same directory as `platformio.ini`. ### Define Your Options -* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available * `FLD_PIN_SCL` - The display SCL pin, defaults to 5 * `FLD_PIN_SDA` - The display SDA pin, defaults to 4 -* `FLIP_MODE` - Set to 0 or 1 -* `LINE_HEIGHT` - Set to 1 or 2 -There are other `#define` values in the Usermod that might be of interest. +All of the parameters can be configured using Usermods settings page, inluding GPIO pins. ### PlatformIO requirements @@ -37,3 +35,5 @@ UI usermod folder for how to include these using `platformio_override.ini`. 2021-02 * First public release +2021-04 +* Adaptation for runtime configuration. \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h index 0b59af5517..999054e5ac 100644 --- a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h +++ b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h @@ -24,50 +24,25 @@ // //The SCL and SDA pins are defined here. -#ifndef FLD_PIN_SCL -#define FLD_PIN_SCL 5 -#endif - -#ifndef FLD_PIN_SDA -#define FLD_PIN_SDA 4 -#endif - -// U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8( -// U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA); -U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8( - U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA); - -// Screen upside down? Change to 0 or 1 -#ifndef FLIP_MODE -#define FLIP_MODE 0 -#endif - -// LINE_HEIGHT 1 is single height, for 128x32 displays. -// LINE_HEIGHT 2 makes the 128x64 screen display at double height. -#ifndef LINE_HEIGHT -#define LINE_HEIGHT 2 -#endif - -// If you aren't also including RotaryEncoderUIUsermod -// you probably want to set both -// SLEEP_MODE_ENABLED false -// CLOCK_MODE_ENABLED false -// as you will never be able wake the display / disable the clock. -#ifdef USERMOD_ROTARY_ENCODER_UI -#ifndef SLEEP_MODE_ENABLED -#define SLEEP_MODE_ENABLED true -#endif -#ifndef CLOCK_MODE_ENABLED -#define CLOCK_MODE_ENABLED true -#endif +#ifdef ARDUINO_ARCH_ESP32 + #ifndef FLD_PIN_SCL + #define FLD_PIN_SCL 22 + #endif + #ifndef FLD_PIN_SDA + #define FLD_PIN_SDA 21 + #endif #else -#define SLEEP_MODE_ENABLED false -#define CLOCK_MODE_ENABLED false + #ifndef FLD_PIN_SCL + #define FLD_PIN_SCL 5 + #endif + #ifndef FLD_PIN_SDA + #define FLD_PIN_SDA 4 + #endif #endif // When to time out to the clock or blank the screen // if SLEEP_MODE_ENABLED. -#define SCREEN_TIMEOUT_MS 15*1000 +#define SCREEN_TIMEOUT_MS 60*1000 // 1 min #define TIME_INDENT 0 #define DATE_INDENT 2 @@ -75,33 +50,43 @@ U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8( // Minimum time between redrawing screen in ms #define USER_LOOP_REFRESH_RATE_MS 1000 -#if LINE_HEIGHT == 2 -#define DRAW_STRING draw1x2String -#define DRAW_GLYPH draw1x2Glyph -#define DRAW_BIG_STRING draw2x2String -#else -#define DRAW_STRING drawString -#define DRAW_GLYPH drawGlyph -#define DRAW_BIG_STRING draw2x2String -#endif - // Extra char (+1) for null #define LINE_BUFFER_SIZE 16+1 -#define FLD_LINE_3_BRIGHTNESS 0 -#define FLD_LINE_3_EFFECT_SPEED 1 -#define FLD_LINE_3_EFFECT_INTENSITY 2 -#define FLD_LINE_3_PALETTE 3 -#if LINE_HEIGHT == 2 -#define TIME_LINE 1 -#else -#define TIME_LINE 0 -#endif +typedef enum { + FLD_LINE_4_BRIGHTNESS = 0, + FLD_LINE_4_EFFECT_SPEED, + FLD_LINE_4_EFFECT_INTENSITY, + FLD_LINE_4_MODE, + FLD_LINE_4_PALETTE +} Line4Type; + +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 +} DisplayType; class FourLineDisplayUsermod : public Usermod { + private: + + bool initDone = false; unsigned long lastTime = 0; + // HW interface & configuration + U8X8 *u8x8 = nullptr; // pointer to U8X8 display object + int8_t sclPin=FLD_PIN_SCL, sdaPin=FLD_PIN_SDA; // I2C pins for interfacing, get initialised in readFromConfig() + DisplayType type = SSD1306; // display type + bool flip = false; // flip display 180° + uint8_t contrast = 10; // screen contrast + uint8_t lineHeight = 1; // 1 row or 2 rows + uint32_t refreshRate = USER_LOOP_REFRESH_RATE_MS; // in ms + uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms + bool sleepMode = true; // allow screen sleep? + bool clockMode = false; // display clock + // needRedraw marks if redraw is required to prevent often redrawing. bool needRedraw = true; @@ -118,38 +103,72 @@ class FourLineDisplayUsermod : public Usermod { uint8_t knownHour = 99; bool displayTurnedOff = false; - long lastUpdate = 0; - long lastRedraw = 0; - long overlayUntil = 0; - byte lineThreeType = FLD_LINE_3_BRIGHTNESS; + unsigned long lastUpdate = 0; + unsigned long lastRedraw = 0; + unsigned long overlayUntil = 0; + Line4Type lineFourType = FLD_LINE_4_BRIGHTNESS; // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. byte markLineNum = 0; - char lineBuffer[LINE_BUFFER_SIZE]; - - char **modes_qstrings = nullptr; - char **palettes_qstrings = nullptr; + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _contrast[]; + static const char _refreshRate[]; + static const char _screenTimeOut[]; + static const char _flip[]; + static const char _sleepMode[]; + static const char _clockMode[]; // 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 + public: // gets called once at boot. Do all initialization that doesn't depend on // network here void setup() { - u8x8.begin(); - u8x8.setFlipMode(FLIP_MODE); - u8x8.setPowerSave(0); - u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 - u8x8.setFont(u8x8_font_chroma48medium8_r); - u8x8.DRAW_STRING(0, 0*LINE_HEIGHT, "Loading..."); - - ModeSortUsermod *modeSortUsermod = (ModeSortUsermod*) usermods.lookup(USERMOD_ID_MODE_SORT); - modes_qstrings = modeSortUsermod->getModesQStrings(); - palettes_qstrings = modeSortUsermod->getPalettesQStrings(); + if (type==NONE) return; + if (!pinManager.allocatePin(sclPin)) { sclPin = -1; type = NONE; return;} + if (!pinManager.allocatePin(sdaPin)) { pinManager.deallocatePin(sclPin); sclPin = sdaPin = -1; type = NONE; return; } + switch (type) { + case SSD1306: + #ifdef ESP8266 + if (!(sclPin==5 && sdaPin==4)) + u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA + break; + case SH1106: + #ifdef ESP8266 + if (!(sclPin==5 && sdaPin==4)) + u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA + break; + case SSD1306_64: + #ifdef ESP8266 + if (!(sclPin==5 && sdaPin==4)) + u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA + break; + default: + u8x8 = nullptr; + type = NONE; + return; + } + (static_cast(u8x8))->begin(); + setFlipMode(flip); + 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..."); + initDone = true; } // gets called every time WiFi is (re-)connected. Initialize own network @@ -160,7 +179,7 @@ class FourLineDisplayUsermod : public Usermod { * Da loop. */ void loop() { - if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { + if (millis() - lastUpdate < (clockMode?1000:refreshRate)) { return; } lastUpdate = millis(); @@ -168,18 +187,59 @@ class FourLineDisplayUsermod : public Usermod { redraw(false); } + /** + * Wrappers for screen drawing + */ + void setFlipMode(uint8_t mode) { + if (type==NONE) return; + (static_cast(u8x8))->setFlipMode(mode); + } + void setContrast(uint8_t contrast) { + if (type==NONE) return; + (static_cast(u8x8))->setContrast(contrast); + } + void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { + if (type==NONE) return; + (static_cast(u8x8))->setFont(u8x8_font_chroma48medium8_r); + if (!ignoreLH && lineHeight==2) (static_cast(u8x8))->draw1x2String(col, row, string); + else (static_cast(u8x8))->drawString(col, row, string); + } + void draw2x2String(uint8_t col, uint8_t row, const char *string) { + if (type==NONE) return; + (static_cast(u8x8))->setFont(u8x8_font_chroma48medium8_r); + (static_cast(u8x8))->draw2x2String(col, row, string); + } + void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) { + if (type==NONE) return; + (static_cast(u8x8))->setFont(font); + if (!ignoreLH && lineHeight==2) (static_cast(u8x8))->draw1x2Glyph(col, row, glyph); + else (static_cast(u8x8))->drawGlyph(col, row, glyph); + } + uint8_t getCols() { + if (type==NONE) return 0; + return (static_cast(u8x8))->getCols(); + } + void clear() { + if (type==NONE) return; + (static_cast(u8x8))->clear(); + } + void setPowerSave(uint8_t save) { + if (type==NONE) return; + (static_cast(u8x8))->setPowerSave(save); + } + /** * Redraw the screen (but only if things have changed * or if forceRedraw). */ void redraw(bool forceRedraw) { + if (type==NONE) return; if (overlayUntil > 0) { if (millis() >= overlayUntil) { // Time to display the overlay has elapsed. overlayUntil = 0; forceRedraw = true; - } - else { + } else { // We are still displaying the overlay // Don't redraw. return; @@ -208,32 +268,52 @@ class FourLineDisplayUsermod : public Usermod { if (!needRedraw) { // Nothing to change. // Turn off display after 3 minutes with no change. - if(SLEEP_MODE_ENABLED && !displayTurnedOff && - (millis() - lastRedraw > SCREEN_TIMEOUT_MS)) { + 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. + knownHour = 99; // force screen clear sleepOrClock(true); - } - else if (displayTurnedOff && CLOCK_MODE_ENABLED) { + } else if (displayTurnedOff && clockMode) { showTime(); + } else if ((millis() - lastRedraw)/1000%3 == 0) { + // change 4th line every 3s + switch (lineFourType) { + case FLD_LINE_4_BRIGHTNESS: + setLineFourType(FLD_LINE_4_EFFECT_SPEED); + break; + case FLD_LINE_4_MODE: + setLineFourType(FLD_LINE_4_BRIGHTNESS); + break; + case FLD_LINE_4_PALETTE: + setLineFourType(clockMode ? FLD_LINE_4_MODE : FLD_LINE_4_BRIGHTNESS); + break; + case FLD_LINE_4_EFFECT_SPEED: + setLineFourType(FLD_LINE_4_EFFECT_INTENSITY); + break; + case FLD_LINE_4_EFFECT_INTENSITY: + setLineFourType(FLD_LINE_4_PALETTE); + break; + default: + break; + } + drawLineFour(); } return; + } else { + knownHour = 99; // force time display + clear(); } + needRedraw = false; lastRedraw = millis(); - if (displayTurnedOff) - { + if (displayTurnedOff) { // Turn the display back on sleepOrClock(false); } // Update last known values. - #if defined(ESP8266) knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); - #else - knownSsid = WiFi.SSID(); - #endif knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); knownBrightness = bri; knownMode = strip.getMode(); @@ -242,79 +322,94 @@ class FourLineDisplayUsermod : public Usermod { knownEffectIntensity = effectIntensity; // Do the actual drawing - u8x8.clear(); - u8x8.setFont(u8x8_font_chroma48medium8_r); // First row with Wifi name - String ssidString = knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0); - u8x8.DRAW_STRING(1, 0*LINE_HEIGHT, ssidString.c_str()); - // Print `~` char to indicate that SSID is longer, than owr dicplay - if (knownSsid.length() > u8x8.getCols()) { - u8x8.DRAW_STRING(u8x8.getCols() - 1, 0*LINE_HEIGHT, "~"); + drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // wifi icon + String ssidString = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); + drawString(1, 0, ssidString.c_str()); + // Print `~` char to indicate that SSID is longer, than our display + if (knownSsid.length() > getCols()) { + drawString(getCols() - 1, 0, "~"); } // Second row with IP or Psssword + drawGlyph(0, lineHeight, 68, u8x8_font_open_iconic_embedded_1x1); // home icon // Print password in AP mode and if led is OFF. if (apActive && bri == 0) { - u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, apPass); - } - else { - String ipString = knownIp.toString(); - u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, ipString.c_str()); + drawString(1, lineHeight, apPass); + } else { + drawString(1, lineHeight, (knownIp.toString()).c_str()); } - // Third row with mode name - showCurrentEffectOrPalette(modes_qstrings[knownMode], 2); + // Third row with mode name or current time + if (clockMode) showTime(false); + else showCurrentEffectOrPalette(knownMode, JSON_mode_names, 2); + + // Fourth row + drawLineFour(); + + drawGlyph(0, 2*lineHeight, 66 + (bri > 0 ? 3 : 0), u8x8_font_open_iconic_weather_2x2); // sun/moon icon + //if (markLineNum>1) drawGlyph(2, markLineNum*lineHeight, 66, u8x8_font_open_iconic_arrow_1x1); // arrow icon + } - switch(lineThreeType) { - case FLD_LINE_3_BRIGHTNESS: - sprintf(lineBuffer, "Brightness %d", bri); - u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + void drawLineFour() { + char lineBuffer[LINE_BUFFER_SIZE]; + switch(lineFourType) { + case FLD_LINE_4_BRIGHTNESS: + sprintf_P(lineBuffer, PSTR("Brightness %3d"), bri); + drawString(2, 3*lineHeight, lineBuffer); + break; + case FLD_LINE_4_EFFECT_SPEED: + sprintf_P(lineBuffer, PSTR("FX Speed %3d"), effectSpeed); + drawString(2, 3*lineHeight, lineBuffer); break; - case FLD_LINE_3_EFFECT_SPEED: - sprintf(lineBuffer, "FX Speed %d", effectSpeed); - u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + case FLD_LINE_4_EFFECT_INTENSITY: + sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity); + drawString(2, 3*lineHeight, lineBuffer); break; - case FLD_LINE_3_EFFECT_INTENSITY: - sprintf(lineBuffer, "FX Intense %d", effectIntensity); - u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + case FLD_LINE_4_MODE: + showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); break; - case FLD_LINE_3_PALETTE: - showCurrentEffectOrPalette(palettes_qstrings[knownPalette], 3); + case FLD_LINE_4_PALETTE: + default: + showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 3); break; } - - u8x8.setFont(u8x8_font_open_iconic_arrow_1x1); - u8x8.DRAW_GLYPH(0, markLineNum*LINE_HEIGHT, 66); // arrow icon - - u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); - u8x8.DRAW_GLYPH(0, 0*LINE_HEIGHT, 80); // wifi icon - u8x8.DRAW_GLYPH(0, 1*LINE_HEIGHT, 68); // home icon } /** * Display the current effect or palette (desiredEntry) * on the appropriate line (row). - * - * TODO: Should we cache the current effect and - * TODO: palette name? This seems expensive. */ - void showCurrentEffectOrPalette(char *qstring, uint8_t row) { - uint8_t printedChars = 1; + void showCurrentEffectOrPalette(int knownMode, const char *qstring, uint8_t row) { + char lineBuffer[LINE_BUFFER_SIZE]; + uint8_t qComma = 0; + bool insideQuotes = false; + uint8_t printedChars = 0; char singleJsonSymbol; - int i = 0; - while (true) { + + // Find the mode name in JSON + for (size_t i = 0; i < strlen_P(qstring); i++) { singleJsonSymbol = pgm_read_byte_near(qstring + i); - if (singleJsonSymbol == '"' || singleJsonSymbol == '\0' ) { - break; - } - u8x8.DRAW_GLYPH(printedChars, row * LINE_HEIGHT, singleJsonSymbol); - printedChars++; - if ( (printedChars > u8x8.getCols() - 2)) { - break; + if (singleJsonSymbol == '\0') break; + switch (singleJsonSymbol) { + case '"': + insideQuotes = !insideQuotes; + break; + case '[': + case ']': + break; + case ',': + qComma++; + default: + if (!insideQuotes || (qComma != knownMode)) break; + lineBuffer[printedChars++] = singleJsonSymbol; } - i++; + if ((qComma > knownMode) || (printedChars >= getCols()-2) || printedChars >= sizeof(lineBuffer)-2) break; } + for (;printedChars < getCols()-2 && printedChars < sizeof(lineBuffer)-2; printedChars++) lineBuffer[printedChars]=' '; + lineBuffer[printedChars] = 0; + drawString(2, row*lineHeight, lineBuffer); } /** @@ -324,6 +419,7 @@ class FourLineDisplayUsermod : public Usermod { * to wake up the screen. */ bool wakeDisplay() { + knownHour = 99; if (displayTurnedOff) { // Turn the display back on sleepOrClock(false); @@ -345,36 +441,31 @@ class FourLineDisplayUsermod : public Usermod { } // Print the overlay - u8x8.clear(); - u8x8.setFont(u8x8_font_chroma48medium8_r); - if (line1) { - u8x8.DRAW_STRING(0, 1*LINE_HEIGHT, line1); - } - if (line2) { - u8x8.DRAW_STRING(0, 2*LINE_HEIGHT, line2); - } + clear(); + if (line1) drawString(0, 1*lineHeight, line1); + if (line2) drawString(0, 2*lineHeight, line2); overlayUntil = millis() + showHowLong; } /** - * Specify what data should be defined on line 3 + * Specify what data should be defined on line 4 * (the last line). */ - void setLineThreeType(byte newLineThreeType) { - if (newLineThreeType == FLD_LINE_3_BRIGHTNESS || - newLineThreeType == FLD_LINE_3_EFFECT_SPEED || - newLineThreeType == FLD_LINE_3_EFFECT_INTENSITY || - newLineThreeType == FLD_LINE_3_PALETTE) { - lineThreeType = newLineThreeType; - } - else { - // Unknown value. - lineThreeType = FLD_LINE_3_BRIGHTNESS; + void setLineFourType(Line4Type newLineFourType) { + if (newLineFourType == FLD_LINE_4_BRIGHTNESS || + newLineFourType == FLD_LINE_4_EFFECT_SPEED || + newLineFourType == FLD_LINE_4_EFFECT_INTENSITY || + newLineFourType == FLD_LINE_4_MODE || + newLineFourType == FLD_LINE_4_PALETTE) { + lineFourType = newLineFourType; + } else { + // Unknown value + lineFourType = FLD_LINE_4_BRIGHTNESS; } } /** - * Line 2 or 3 (last two lines) can be marked with an + * Line 3 or 4 (last two lines) can be marked with an * arrow in the first column. Pass 2 or 3 to this to * specify which line to mark with an arrow. * Any other values are ignored. @@ -388,42 +479,17 @@ class FourLineDisplayUsermod : public Usermod { } } - /* - * 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 - } - */ - /** * Enable sleep (turn the display off) or clock mode. */ void sleepOrClock(bool enabled) { if (enabled) { - if (CLOCK_MODE_ENABLED) { - showTime(); - } - else { - u8x8.setPowerSave(1); - } + if (clockMode) showTime(); + else setPowerSave(1); displayTurnedOff = true; } else { - if (!CLOCK_MODE_ENABLED) { - u8x8.setPowerSave(0); - } + setPowerSave(0); displayTurnedOff = false; } } @@ -433,23 +499,28 @@ class FourLineDisplayUsermod : public Usermod { * on the middle rows. Based 24 or 12 hour depending on * the useAMPM configuration. */ - void showTime() { + void showTime(bool fullScreen = true) { + char lineBuffer[LINE_BUFFER_SIZE]; + updateLocalTime(); byte minuteCurrent = minute(localTime); byte hourCurrent = hour(localTime); + byte secondCurrent = second(localTime); if (knownMinute == minuteCurrent && knownHour == hourCurrent) { // Time hasn't changed. - return; + if (!fullScreen) return; + } else { + if (fullScreen) clear(); } knownMinute = minuteCurrent; knownHour = hourCurrent; - u8x8.clear(); - u8x8.setFont(u8x8_font_chroma48medium8_r); - - int currentMonth = month(localTime); - sprintf(lineBuffer, "%s %d", monthShortStr(currentMonth), day(localTime)); - u8x8.DRAW_BIG_STRING(DATE_INDENT, TIME_LINE*LINE_HEIGHT, lineBuffer); + byte currentMonth = month(localTime); + sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(currentMonth), day(localTime)); + if (fullScreen) + draw2x2String(DATE_INDENT, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays + else + drawString(2, lineHeight*2, lineBuffer); byte showHour = hourCurrent; boolean isAM = false; @@ -467,25 +538,45 @@ class FourLineDisplayUsermod : public Usermod { } } - sprintf(lineBuffer, "%02d:%02d %s", showHour, minuteCurrent, useAMPM ? (isAM ? "AM" : "PM") : ""); + sprintf_P(lineBuffer, (secondCurrent%2 || !fullScreen) ? PSTR("%2d:%02d") : PSTR("%2d %02d"), (useAMPM ? showHour : hourCurrent), minuteCurrent); // For time, we always use LINE_HEIGHT of 2 since // we are printing it big. - u8x8.DRAW_BIG_STRING(TIME_INDENT + (useAMPM ? 0 : 2), (TIME_LINE + 1) * 2, lineBuffer); + if (fullScreen) { + draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer); + sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); + if (!useAMPM) drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line + } else { + drawString(9+(useAMPM?0:2), lineHeight*2, lineBuffer); + } + if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*2, (isAM ? "AM" : "PM"), true); } + /* + * 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.")); + //} + /* * 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) { - } + //void readFromJsonState(JsonObject& root) { + // if (!initDone) return; // prevent crash on boot applyPreset() + //} /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -502,6 +593,18 @@ 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) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + JsonArray i2c_pin = top.createNestedArray("pin"); + i2c_pin.add(sclPin); + i2c_pin.add(sdaPin); + top["type"] = type; + top[FPSTR(_flip)] = (bool) flip; + top[FPSTR(_contrast)] = contrast; + top[FPSTR(_refreshRate)] = refreshRate/1000; + top[FPSTR(_screenTimeOut)] = screenTimeout/1000; + top[FPSTR(_sleepMode)] = (bool) sleepMode; + top[FPSTR(_clockMode)] = (bool) clockMode; + DEBUG_PRINTLN(F("4 Line Display config saved.")); } /* @@ -513,6 +616,74 @@ class FourLineDisplayUsermod : public Usermod { * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) */ void readFromConfig(JsonObject& root) { + bool needsRedraw = false; + DisplayType newType = type; + int8_t newScl = sclPin; + int8_t newSda = sdaPin; + + JsonObject top = root[FPSTR(_name)]; + if (!top.isNull() && top["pin"] != nullptr) { + newScl = top["pin"][0]; + newSda = top["pin"][1]; + newType = top["type"]; + if (top[FPSTR(_flip)].is()) { + flip = top[FPSTR(_flip)].as(); + } else { + String str = top[FPSTR(_flip)]; // checkbox -> off or on + flip = (bool)(str!="off"); // off is guaranteed to be present + needRedraw |= true; + } + contrast = top[FPSTR(_contrast)].as(); + refreshRate = top[FPSTR(_refreshRate)].as() * 1000; + screenTimeout = top[FPSTR(_screenTimeOut)].as() * 1000; + if (top[FPSTR(_sleepMode)].is()) { + sleepMode = top[FPSTR(_sleepMode)].as(); + } else { + String str = top[FPSTR(_sleepMode)]; // checkbox -> off or on + sleepMode = (bool)(str!="off"); // off is guaranteed to be present + needRedraw |= true; + } + if (top[FPSTR(_clockMode)].is()) { + clockMode = top[FPSTR(_clockMode)].as(); + } else { + String str = top[FPSTR(_clockMode)]; // checkbox -> off or on + clockMode = (bool)(str!="off"); // off is guaranteed to be present + needRedraw |= true; + } + DEBUG_PRINTLN(F("4 Line Display config (re)loaded.")); + } else { + DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + } + + if (!initDone) { + // first run: reading from cfg.json + sclPin = newScl; + sdaPin = newSda; + type = newType; + lineHeight = type==SSD1306 ? 1 : 2; + } else { + // changing paramters from settings page + if (sclPin!=newScl || sdaPin!=newSda || type!=newType) { + if (type==SSD1306) delete (static_cast(u8x8)); + if (type==SH1106) delete (static_cast(u8x8)); + if (type==SSD1306_64) delete (static_cast(u8x8)); + pinManager.deallocatePin(sclPin); + pinManager.deallocatePin(sdaPin); + sclPin = newScl; + sdaPin = newSda; + if (newScl<0 || newSda<0) { + type = NONE; + return; + } else + type = newType; + lineHeight = type==SSD1306 ? 1 : 2; + setup(); + needRedraw |= true; + } + setContrast(contrast); + setFlipMode(flip); + if (needsRedraw && !wakeDisplay()) redraw(true); + } } /* @@ -522,5 +693,13 @@ class FourLineDisplayUsermod : public Usermod { uint16_t getId() { return USERMOD_ID_FOUR_LINE_DISP; } - -}; \ No newline at end of file +}; + +// strings to reduce flash memory usage (used more than twice) +const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay"; +const char FourLineDisplayUsermod::_contrast[] PROGMEM = "contrast"; +const char FourLineDisplayUsermod::_refreshRate[] PROGMEM = "refreshRateSec"; +const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec"; +const char FourLineDisplayUsermod::_flip[] PROGMEM = "flip"; +const char FourLineDisplayUsermod::_sleepMode[] PROGMEM = "sleepMode"; +const char FourLineDisplayUsermod::_clockMode[] PROGMEM = "clockMode"; diff --git a/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample index cc39d65cf9..4b537a8f7c 100644 --- a/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample +++ b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample @@ -9,7 +9,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp32} -D USERMOD_MODE_SORT - -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 + -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19 -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 -D LEDPIN=16 -D BTNPIN=13 @@ -28,7 +28,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D USERMOD_MODE_SORT - -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 + -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=12 -D ENCODER_CLK_PIN=14 -D ENCODER_SW_PIN=13 -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 -D LEDPIN=3 -D BTNPIN=0 diff --git a/usermods/usermod_v2_rotary_encoder_ui/readme.md b/usermods/usermod_v2_rotary_encoder_ui/readme.md index 4477590b58..9bbcd6a6ca 100644 --- a/usermods/usermod_v2_rotary_encoder_ui/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui/readme.md @@ -16,7 +16,7 @@ This file should be placed in the same directory as `platformio.ini`. ### Define Your Options * `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) * `ENCODER_DT_PIN` - The encoders DT pin, defaults to 12 * `ENCODER_CLK_PIN` - The encoders CLK pin, defaults to 14 * `ENCODER_SW_PIN` - The encoders SW pin, defaults to 13 diff --git a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h index 6dc2a1be3f..5ff1090d22 100644 --- a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h +++ b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h @@ -37,7 +37,7 @@ #define ENCODER_SW_PIN 13 #endif -#ifndef USERMOD_FOUR_LINE_DISLAY +#ifndef USERMOD_FOUR_LINE_DISPLAY // These constants won't be defined if we aren't using FourLineDisplay. #define FLD_LINE_3_BRIGHTNESS 0 #define FLD_LINE_3_EFFECT_SPEED 0 @@ -62,7 +62,7 @@ class RotaryEncoderUIUsermod : public Usermod { unsigned char button_state = HIGH; unsigned char prev_button_state = HIGH; -#ifdef USERMOD_FOUR_LINE_DISLAY +#ifdef USERMOD_FOUR_LINE_DISPLAY FourLineDisplayUsermod *display; #else void* display = nullptr; @@ -96,7 +96,7 @@ class RotaryEncoderUIUsermod : public Usermod { modes_alpha_indexes = modeSortUsermod->getModesAlphaIndexes(); palettes_alpha_indexes = modeSortUsermod->getPalettesAlphaIndexes(); -#ifdef USERMOD_FOUR_LINE_DISLAY +#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); @@ -248,7 +248,7 @@ class RotaryEncoderUIUsermod : public Usermod { } boolean changeState(const char *stateName, byte lineThreeMode, byte markedLine) { -#ifdef USERMOD_FOUR_LINE_DISLAY +#ifdef USERMOD_FOUR_LINE_DISPLAY if (display != nullptr) { if (display->wakeDisplay()) { // Throw away wake up input @@ -272,7 +272,7 @@ class RotaryEncoderUIUsermod : public Usermod { } void changeBrightness(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISLAY +#ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { // Throw away wake up input return; @@ -288,7 +288,7 @@ class RotaryEncoderUIUsermod : public Usermod { } void changeEffect(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISLAY +#ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { // Throw away wake up input return; @@ -305,7 +305,7 @@ class RotaryEncoderUIUsermod : public Usermod { } void changeEffectSpeed(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISLAY +#ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { // Throw away wake up input return; @@ -321,7 +321,7 @@ class RotaryEncoderUIUsermod : public Usermod { } void changeEffectIntensity(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISLAY +#ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { // Throw away wake up input return; @@ -337,7 +337,7 @@ class RotaryEncoderUIUsermod : public Usermod { } void changePalette(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISLAY +#ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { // Throw away wake up input return; diff --git a/wled00/.vs/wled00/v15/.suo b/wled00/.vs/wled00/v15/.suo deleted file mode 100644 index 5bbdedd721cd1323926d95284350e94ada9b9459..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41984 zcmeHQYm{5nb-rV4{Gyl?LI_D=z!)4nlCLzLX$KJw56p@+ax5=ltO9Rl+vUnZqq>9wIH;m4b2buk(FOfRv$mo)x_@i-FwgI z>W&^)8b4-+duDxeU+12Ep8M>x&)!#Wy#Bs-zVPDp?@ESnmvotQ_S|Oaa^3wl-MMs? zByGj@I^gWNbLaSo&v9ynRa65Qo9tEvN-0YSvIBVaILgqvCTI&c&q)nZTm9* ze`)&w;yvV4Blm)0PC6mo1L`9<3rjN%QmK4=Eqb87x+|Ynzj_U~(S4P0SSq2^87U3= z#HbX-bsR`3dC{_|#a?>2MOxRk3%9Qan2)6~s^3}AZa1ReS^f3JyQJT2`8j4ksina2 z{WDzx9Lo$>fDYhVfN2?n9ry15b^})e4nXAD7}t0mL0sd#TG#J45w{m{ zWE1gf(O3!hFXs5a>PuU!@sIw(uu{gq=!2V(C&35Ac;dJhe1PMhachnL4S3G+&v!Zg z8MoE(zZ37@25>(42yi>Vv3?D}bUFTcY&Kko@lW30Y(7>}{Es95e+RJLh+o3_F9VMP zp8$RX_!WT9K853_we!zt$IofUCvf~V;McY5Cvp5u;J1LMfZqmw2lzbjyTI=OzYlx? z_#*Hn;A!B?z*m50fMU?00|Ft*%XSMNf|m4fFtq0O~mO<*s%-F;3Pm9l|7{h%m7osG%yd4bIk##fJcA@ z;A6lq0uKREP)=yQZVJ@c3n_aU`GOs#S9>W(X-OHOcpY`A>Yv$8$nrVfYZOi+0{a$2 zGxRx5i)bxMf?4F6^|P;q{{JxEBfns{A1$6lxr-=OUU~q1vRmN?>W+x|g!giwM|n_{ z&eV7L-`fKH7x3PVS`zivA6yf$f7c7G@Sn$!Ap0GIv7H4E=6WDwVkKyAq5S=zOI;p= zk>814#{D2;-ncI1+@KGT|J%q<$eYaezdpoWuO(7RzYl$&r1UbeAC*^DW=q>$dmTJz zUE7toAYU{~pJQ9RA3}+SL331DdFQbnVlQLgZY24uDYUmx|53au>OVPka{m#(-yhf= z?LQhA2#xhRj`?RoIsbGpJea@F>B|j;X5;DI`}Ys^4ebt0pNPaWW4>T~f27zw>)d_s zoguK-7}yP$DoLCTVz7k4O3U|TR8r-Y9m6P#4t9h4?eD$(*uuW!4?i~d=1k%Z7KxRp z56a5Z37+l&pYK9K3A9lW#{!NC>7lluk^!|HDMfIVs9!y`V;-5oIPJn*5(B@_Bby~H z12JSfjr!+Xd7QD%8K(fgNeCWZabS+JYpZFsO#95%-ywAVVbm_OI;jiwe|PgqH@$e-%x(G{(|9 z`u?x#eW>%H45o5dv$UD=PK-aUr71J8+wfkaun`f)*Qv1@@n6ai^+GHB_d7_C^ZEG- z6~m>;MA|FQnb}Ji18T~8eN-==u}=!_Urib(|KFz3He3Et z#D8`x{vp((-w*1n8KH?%->CeL`L~U7_;r9;`uh5B)c;+Tt42B0Jy9>P{H%AbOU%~)XAxJ(UybN<9aArmKG#KD1DU1& zIO6_POSBC~_6N2hWg?ynP)6SjSnI#FyZ-A#Pb`4{cZ1K7_eUX8a3w?;l$z0MJP%!B zF^K-pzQ?fmpw7vk;+a_g4S<5szz>7+401r8$F*o;ag^3hjQXFe@%ZNfv-H1)xI+Hd z>;L;u;xt67%6$O)BLA(n6W{`UvT~*HCd>ak;{y!(`PYvUbFP^}xhWSFK{d#Ud>hjcY8fbGY8& zoX2_R?(e?vsd(|hXEyz?`-zv{|Fx2(|0(lN*5XRn|M*9|yM1F=0Q&tyhoVR2d;Ix= zE78Rzq5o+1NWUZC_XlTAOiy|5JJQda_a?$)#}djqB1 z9AP{%T)~COOiwzI&lPebMf*UyP>N@w#Zoezvme1}x;WdH%Vi4ozVu|lej{RYEE3b1 zRMO!)exi`e+SA$G7jdg$FzO#De`m2ajA8{mfdwbEK=lIl9~JrMHbiQL{}}1_fa6Ul zGtev+yJ}vzH(YBT;{F458H-G+US_ptjNhl78|hQKn8KVm4!)aH=yML#=cJMV`2Qkp zv*qV}CFVaPeU%btV$~ADk9&%@Ip{3O4fch^{cC++3SBm*>$gKY1I2M0XXqp&A zeWC54X2*`Z1yq<`twO!TXU^Uo-uk~MuX@?{-M+WJ^IC|R)@M}9?1)+VDP=;Wm@*7{ zOI0yKA++>~StX6WT1}rPQzP+Grg*$KF&PX7!uDdSP%Na1#dLPGR+p_vn><1c{fY-> zFO2+SJ#YMHTXeQiOikE>xlAUND5i7Sf_;A~o64sX_E1{65zo&)=)C`aEn>8oN0vL< zCklyNK9e5qXdg)B3wUp@%P#Y;qdkanmh!2+*;J{Rk7qjCdrQNabmCwN{AfIt-8<~@ z#N7#Zm%|5UD|>wpbcn?Dxi}Cnj1}X|MlH{qg1L#wcs`ZiL~7CWTnc=6 z)8q9jMXQ|fNtMHj1Riu;&>DI`VFR1V@)_3iWF=;kncG^SH;iG$DMK41Z~V!(5C6|h z=$p@vxPE-*?>`}Qc+};@G_9wQ?>TIHkcwUx%9QPjW>@HU$@3FSUO zC6qOkxGPC-u9=dhVg1DYZMN6jO_r*W{TcO;^#a%bwDqE$irMw2UjNvLKG(-nNZnN< z6I}n3uG}L11CR!jilj+MAzxs%6S(kM{>{2`5YE9DPh=}gZHS}x=u^HTRB zY+&l``-yz9{<)T?9>^^H0mKz{MU==J$;cN+h7==Hwd+tVGqR|e9P}U5!K|%WgBl*L zkeVN{E4Rg_(38n!^C%hBG%Vfsfz`PVF!o25j@DtSZ3OKFSiaReKhnA*gIRtAz~PsdSvxolJAzTeC_(oPd9Hr z{MmDl{^a#DPt&9tcYk=zjyGS}zJ34KfAH?YZNn4VgiiepZHLUNftePp!zOSSN|S7g86xD3Okcqfd#&J_U~G zhMeI=Ekn<=Wr-3^BqWTK%SFQ(E%JhjO`aFl+`Djxc~GdgNmuAq^M*^B!82vz8ht!F zW>-o+H6PGmmk$q=dx&~gzju_M8s9@&kmw??GK@;MZ2g&k*QdXA;KjGU`uf#h9^BSk z|A|W8k!#MJJ#$0%)1Oa1|Ktrn_BHoDt^HkEUU>ANRcx>^521#`IP1b+C+>$4ud?Q8 z7m!l)c0K4d8T5{{p&yxPe-BcZL8tXxB(WV+**8`HU45@(52=hJ@}_!xGVUhDhEm@* zmz%dclwHyDnW7!uk}1?Rcj#yvH?Qr?bz)4|F_Tf6R;{N4h?PXC%VU@ux92x6<-NOR z@gsfbBSB}-@AWRstBKgB5;K$e+==Zja?Y4x2~yla;3f3pD91MIIWwsot=s=Rv?m%^^oNT2!!+I^Y*0)enU z;M$j=l2Y}nBe-2_|6R4aqAhR|xkxH@SoQKl|8tJ$?SNVOy>;l9?JuYepofF7RK-hG z0?My^Q>z`%%+kLWaR+F`gnMsm+Vl-Tu#{PSe zc1p3yc(Tx*j2E#LpGp$pC{$uktWiL_8Do=aVJOVGcJ6m>7?brV6oCVY1U18;j?Y ztS{}-L3@iBj4<-?d>WC$(cX#Bp=hj-Pl$8OL8mzBGAOgJ;h*|MmKWyb4tX9vTZPm~ zWU$b_b23+m(PjnhfCf@(z;HQ|%TG8^4`%CsK7tnlwrHO!jU*EZ zS1Rs9BP8Sm8o@bi8+OWWTgvUio#cqmU? z*`+4hS0k&7qNT7y6ZXR~%y+bWHf!iL5r>^qzHEPN!r~HHDhH$Rx@|FMsuAV(Y8Bl= zoJTGbL((u(l}qu=uAa=N2LgwWPRj@PkLGBJdMMgI7VaO#8LV*;js*Sp@pv9=cblti zy;mg7>eSjkU|M@4v3_TR2H*xqUT{;`j4d+yyccg?QBoT%wEYwh(070RdP-L z@3-32-67%Y$bCNUjq#h*^hBaLFD;vf9#D3(x7(eyM#QEcwrp|1O~XM^l4ZlceIRV#$9(_T$l#|19|r z-N3L!5ItmJ-r?%%aH_*qlK_`1|7oJP<^M@W1#q)E0_+D40QUg&=Q{{=13kbY;4mQk ze=~uIV zrm-xR|KIB9bC&;KtKUetIK13AuINK#`G2Xu0CV9+7_11E{q3vMkXv6-meC)0<#C*I zKRvWEEO4*T+r+e2+lgo@!b-K`X#ERp>Q{Hoe5qyWS3is+*7E<$DSo`GIUOggXRBc(*Mx%|Klh8EdM|HB3k}`=hs8d^8XuyKb6rB(enRm z$q~`=|2uCEntJ~~%l>zH_P;l3_P^`l0c83A(ErNv|Is;J)pDv?)BZo5T^$b1RVhUu z5$c1+YIt?1U)X@>hxYpcmj4f|{Vt;aPwSc^>-XP!6_2CU{rhifD_&|Zg!7?=<}VeA zz%OEK1VSs}TNGQ$9+V9n#u_|cd)nu<)B7&f?|*z4y`27M{2c-PRtA6X*i5kezuw#a zU+07KTKj*h$A<9JxcFW?%a!7+`M;@;Vl}6WvgZE=zahOAdf#fD|NBbm$zJtqH07WC zakx$Y=8xPt;vVsgI2=j09Pf;8;P({v*7)@@YT9%^U8W;#kCt-$APqm3!xV11lX0Wk z?Ba=ymGjTMxHD!=J<7IK0B?Q9iEN$_^+_ z@2WSsqi*6{wfviA^}W`7;MFyl`R{p%PgL-0q2+Iauso<&E4$Jl&RV&e(%%V z&)C=c8FqZ>1Nl7Nr2T&7_Y10jbD+6jqgQ{~t(LZJX?!>?*qv*f+>{Qv&}uzviH diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 21d156b1b0..9a399afd2a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -34,7 +34,7 @@ */ uint16_t WS2812FX::mode_static(void) { fill(SEGCOLOR(0)); - return (SEGMENT.getOption(SEG_OPTION_TRANSITIONAL)) ? FRAMETIME : 380; //update faster if in transition + return (SEGMENT.getOption(SEG_OPTION_TRANSITIONAL)) ? FRAMETIME : 350; //update faster if in transition } @@ -42,30 +42,24 @@ uint16_t WS2812FX::mode_static(void) { * Blink/strobe function * Alternate between color1 and color2 * if(strobe == true) then create a strobe effect - * NOTE: Maybe re-rework without timer */ uint16_t WS2812FX::blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { - uint16_t stateTime = SEGENV.aux1; uint32_t cycleTime = (255 - SEGMENT.speed)*20; - uint32_t onTime = 0; - uint32_t offTime = cycleTime; - - if (!strobe) { - onTime = (cycleTime * SEGMENT.intensity) >> 8; - offTime = cycleTime - onTime; - } + uint32_t onTime = FRAMETIME; + if (!strobe) onTime += ((cycleTime * SEGMENT.intensity) >> 8); + cycleTime += FRAMETIME*2; + uint32_t it = now / cycleTime; + uint32_t rem = now % cycleTime; - stateTime = ((SEGENV.aux0 & 1) == 0) ? onTime : offTime; - stateTime += 20; - - if (now - SEGENV.step > stateTime) - { - SEGENV.aux0++; - SEGENV.aux1 = stateTime; - SEGENV.step = now; + bool on = false; + if (it != SEGENV.step //new iteration, force on state for one frame, even if set time is too brief + || rem <= onTime) { + on = true; } + + SEGENV.step = it; //save previous iteration - uint32_t color = ((SEGENV.aux0 & 1) == 0) ? color1 : color2; + uint32_t color = on ? color1 : color2; if (color == color1 && do_palette) { for(uint16_t i = 0; i < SEGLEN; i++) { @@ -1467,8 +1461,8 @@ uint16_t WS2812FX::mode_tricolor_fade(void) } byte stp = prog; // % 256 - uint32_t color = 0; for(uint16_t i = 0; i < SEGLEN; i++) { + uint32_t color; if (stage == 2) { color = color_blend(color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp); } else if (stage == 1) { @@ -3085,7 +3079,7 @@ uint16_t WS2812FX::mode_drip(void) numDrops = 1 + (SEGMENT.intensity >> 6); - float gravity = -0.001 - (SEGMENT.speed/50000.0); + float gravity = -0.0005 - (SEGMENT.speed/50000.0); gravity *= SEGLEN; int sourcedrop = 12; diff --git a/wled00/FX.h b/wled00/FX.h index 44eea038a8..77638aedf4 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -75,7 +75,7 @@ #define SEGENV _segment_runtimes[_segment_index] #define SEGLEN _virtualSegmentLength #define SEGACT SEGMENT.stop -#define SPEED_FORMULA_L 5 + (50*(255 - SEGMENT.speed))/SEGLEN +#define SPEED_FORMULA_L 5U + (50U*(255U - SEGMENT.speed))/SEGLEN #define RESET_RUNTIME memset(_segment_runtimes, 0, sizeof(_segment_runtimes)) // some common colors @@ -581,7 +581,7 @@ class WS2812FX { } void - finalizeInit(uint16_t countPixels, bool skipFirst), + finalizeInit(uint16_t countPixels), service(void), blur(uint8_t), fill(uint32_t), @@ -606,6 +606,7 @@ class WS2812FX { bool isRgbw = false, + isOffRefreshRequred = false, //periodic refresh is required for the strip to remain off. gammaCorrectBri = false, gammaCorrectCol = true, applyToAllSelected = true, @@ -812,7 +813,6 @@ class WS2812FX { void handle_palette(void); bool - _skipFirstMode, _triggered; mode_ptr _mode[MODE_COUNT]; // SRAM footprint: 4 bytes per element diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 7d964cb493..075988cd58 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -49,7 +49,7 @@ //#define DEFAULT_LED_TYPE TYPE_WS2812_RGB #ifndef PIXEL_COUNTS - #define PIXEL_COUNTS 30 + #define PIXEL_COUNTS DEFAULT_LED_COUNT #endif #ifndef DATA_PINS @@ -61,16 +61,11 @@ #endif //do not call this method from system context (network callback) -void WS2812FX::finalizeInit(uint16_t countPixels, bool skipFirst) +void WS2812FX::finalizeInit(uint16_t countPixels) { RESET_RUNTIME; _length = countPixels; - _skipFirstMode = skipFirst; - _lengthRaw = _length; - if (_skipFirstMode) { - _lengthRaw += LED_SKIP_AMOUNT; - } //if busses failed to load, add default (FS issue...) if (busses.getNumBusses() == 0) { @@ -204,7 +199,6 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) } } - uint16_t skip = _skipFirstMode ? LED_SKIP_AMOUNT : 0; if (SEGLEN) {//from segment //color_blend(getpixel, col, _bri_t); (pseudocode for future blending of segments) @@ -216,7 +210,6 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) } uint32_t col = ((w << 24) | (r << 16) | (g << 8) | (b)); - /* Set all the pixels in the group, ensuring _skipFirstMode is honored */ bool reversed = IS_REVERSE; uint16_t realIndex = realPixelIndex(i); @@ -226,22 +219,17 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) if (IS_MIRROR) { //set the corresponding mirrored pixel uint16_t indexMir = SEGMENT.stop - indexSet + SEGMENT.start - 1; if (indexMir < customMappingSize) indexMir = customMappingTable[indexMir]; - busses.setPixelColor(indexMir + skip, col); + busses.setPixelColor(indexMir, col); } if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; - busses.setPixelColor(indexSet + skip, col); + busses.setPixelColor(indexSet, col); } } } else { //live data, etc. if (i < customMappingSize) i = customMappingTable[i]; uint32_t col = ((w << 24) | (r << 16) | (g << 8) | (b)); - busses.setPixelColor(i + skip, col); - } - if (skip && i == 0) { - for (uint16_t j = 0; j < skip; j++) { - busses.setPixelColor(j, BLACK); - } + busses.setPixelColor(i, col); } } @@ -514,8 +502,6 @@ uint32_t WS2812FX::getPixelColor(uint16_t i) i = realPixelIndex(i); if (i < customMappingSize) i = customMappingTable[i]; - - if (_skipFirstMode) i += LED_SKIP_AMOUNT; if (i >= _lengthRaw) return 0; @@ -978,7 +964,7 @@ uint32_t WS2812FX::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8 } uint8_t paletteIndex = i; - if (mapping) paletteIndex = (i*255)/(SEGLEN -1); + if (mapping && SEGLEN > 1) paletteIndex = (i*255)/(SEGLEN -1); if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" CRGB fastled_col; fastled_col = ColorFromPalette( currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND); diff --git a/wled00/__vm/.wled00.vsarduino.h b/wled00/__vm/.wled00.vsarduino.h deleted file mode 100644 index 2a9ef40a43..0000000000 --- a/wled00/__vm/.wled00.vsarduino.h +++ /dev/null @@ -1,124 +0,0 @@ -/* - Editor: https://www.visualmicro.com/ - This file is for intellisense purpose only. - Visual micro (and the arduino ide) ignore this code during compilation. This code is automatically maintained by visualmicro, manual changes to this file will be overwritten - The contents of the _vm sub folder can be deleted prior to publishing a project - All non-arduino files created by visual micro and all visual studio project or solution files can be freely deleted and are not required to compile a sketch (do not delete your own code!). - Note: debugger breakpoints are stored in '.sln' or '.asln' files, knowledge of last uploaded breakpoints is stored in the upload.vmps.xml file. Both files are required to continue a previous debug session without needing to compile and upload again - - Hardware: ESP32 Dev Module, Platform=esp32, Package=esp32 -*/ - -#if defined(_VMICRO_INTELLISENSE) - -#ifndef _VSARDUINO_H_ -#define _VSARDUINO_H_ -#define __ESP32_esp32__ -#define __ESP32_ESP32__ -#define ESP_PLATFORM -#define HAVE_CONFIG_H -#define GCC_NOT_5_2_0 0 -#define WITH_POSIX -#define F_CPU 240000000L -#define ARDUINO 108011 -#define ARDUINO_ESP32_DEV -#define ARDUINO_ARCH_ESP32 -#define ESP32 -#define CORE_DEBUG_LEVEL 0 -#define __cplusplus 201103L - -#define _Pragma(x) -#undef __cplusplus -#define __cplusplus 201103L - -#define __STDC__ -#define __ARM__ -#define __arm__ -#define __inline__ -#define __asm__(...) -#define __extension__ -#define __ATTR_PURE__ -#define __ATTR_CONST__ -#define __volatile__ - -#define __ASM -#define __INLINE -#define __attribute__(noinline) - -//#define _STD_BEGIN -//#define EMIT -#define WARNING -#define _Lockit -#define __CLR_OR_THIS_CALL -#define C4005 -#define _NEW - -typedef bool _Bool; -typedef int _read; -typedef int _seek; -typedef int _write; -typedef int _close; -typedef int __cleanup; - -//#define inline - -#define __builtin_clz -#define __builtin_clzl -#define __builtin_clzll -#define __builtin_labs -#define __builtin_va_list -typedef int __gnuc_va_list; - -#define __ATOMIC_ACQ_REL - -#define __CHAR_BIT__ -#define _EXFUN() - -typedef unsigned char byte; -extern "C" void __cxa_pure_virtual() {;} - -typedef long __INTPTR_TYPE__ ; -typedef long __UINTPTR_TYPE__ ; -typedef long __SIZE_TYPE__ ; -typedef long __PTRDIFF_TYPE__; - -typedef long pthread_t; -typedef long pthread_key_t; -typedef long pthread_once_t; -typedef long pthread_mutex_t; -typedef long pthread_mutex_t; -typedef long pthread_cond_t; - - - -#include "arduino.h" -#include - -#define interrupts() sei() -#define noInterrupts() cli() - -#define ESP_LOGI(tag, ...) - -#include "wled00.ino" -#include "wled01_eeprom.ino" -#include "wled02_xml.ino" -#include "wled03_set.ino" -#include "wled04_file.ino" -#include "wled05_init.ino" -#include "wled06_usermod.ino" -#include "wled07_notify.ino" -#include "wled08_led.ino" -#include "wled09_button.ino" -#include "wled10_ntp.ino" -#include "wled11_ol.ino" -#include "wled12_alexa.ino" -#include "wled13_cronixie.ino" -#include "wled14_colors.ino" -#include "wled15_hue.ino" -#include "wled16_blynk.ino" -#include "wled17_mqtt.ino" -#include "wled18_server.ino" -#include "wled19_json.ino" -#include "wled20_ir.ino" -#endif -#endif diff --git a/wled00/__vm/Compile.vmps.xml b/wled00/__vm/Compile.vmps.xml deleted file mode 100644 index 6390398ede..0000000000 --- a/wled00/__vm/Compile.vmps.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/wled00/__vm/Configuration.Release.vmps.xml b/wled00/__vm/Configuration.Release.vmps.xml deleted file mode 100644 index bcbca636a5..0000000000 --- a/wled00/__vm/Configuration.Release.vmps.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index 4f406a3f62..83e7c25f10 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -46,7 +46,10 @@ void onAlexaChange(EspalexaDevice* dev) bri = briLast; colorUpdated(NOTIFIER_CALL_MODE_ALEXA); } - } else applyPreset(macroAlexaOn); + } else { + applyPreset(macroAlexaOn); + if (bri == 0) espalexaDevice->setValue(briLast); //stop Alexa from complaining if macroAlexaOn does not actually turn on + } } else if (m == EspalexaDeviceProperty::off) { if (!macroAlexaOff) @@ -57,7 +60,10 @@ void onAlexaChange(EspalexaDevice* dev) bri = 0; colorUpdated(NOTIFIER_CALL_MODE_ALEXA); } - } else applyPreset(macroAlexaOff); + } else { + applyPreset(macroAlexaOff); + if (bri != 0) espalexaDevice->setValue(0); //stop Alexa from complaining if macroAlexaOff does not actually turn off + } } else if (m == EspalexaDeviceProperty::bri) { bri = espalexaDevice->getValue(); diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 89f3e9079d..e8d569db0d 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -17,12 +17,14 @@ struct BusConfig { uint16_t start = 0; uint8_t colorOrder = COL_ORDER_GRB; bool reversed = false; + uint8_t skipAmount; uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false) { - type = busType; count = len; start = pstart; colorOrder = pcolorOrder; reversed = rev; + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip=0) { + type = busType; count = len; start = pstart; + colorOrder = pcolorOrder; reversed = rev; skipAmount = skip; uint8_t nPins = 1; if (type > 47) nPins = 2; - else if (type > 41 && type < 46) nPins = NUM_PWM_PINS(type); + else if (type > 40 && type < 46) nPins = NUM_PWM_PINS(type); for (uint8_t i = 0; i < nPins; i++) pins[i] = ppins[i]; } }; @@ -51,11 +53,11 @@ class Bus { virtual uint8_t getPins(uint8_t* pinArray) { return 0; } - uint16_t getStart() { + inline uint16_t getStart() { return _start; } - void setStart(uint16_t start) { + inline void setStart(uint16_t start) { _start = start; } @@ -69,14 +71,28 @@ class Bus { return COL_ORDER_RGB; } - uint8_t getType() { + virtual bool isRgbw() { + return false; + } + + virtual uint8_t skippedLeds() { + return 0; + } + + inline uint8_t getType() { return _type; } - bool isOk() { + inline bool isOk() { return _valid; } + static bool isRgbw(uint8_t type) { + if (type == TYPE_SK6812_RGBW || type == TYPE_TM1814) return true; + if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; + return false; + } + bool reversed = false; protected: @@ -99,8 +115,9 @@ class BusDigital : public Bus { cleanup(); return; } } - _len = bc.count; reversed = bc.reversed; + _skip = bc.skipAmount; //sacrificial pixels + _len = bc.count + _skip; _iType = PolyBus::getI(bc.type, _pins, nr); if (_iType == I_NONE) return; _busPtr = PolyBus::create(_iType, _pins, _len); @@ -109,11 +126,11 @@ class BusDigital : public Bus { //Serial.printf("Successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u)\n",nr, len, type, pins[0],pins[1],_iType); }; - void show() { + inline void show() { PolyBus::show(_busPtr, _iType); } - bool canShow() { + inline bool canShow() { return PolyBus::canShow(_busPtr, _iType); } @@ -130,20 +147,22 @@ class BusDigital : public Bus { void setPixelColor(uint16_t pix, uint32_t c) { if (reversed) pix = _len - pix -1; + else pix += _skip; PolyBus::setPixelColor(_busPtr, _iType, pix, c, _colorOrder); } uint32_t getPixelColor(uint16_t pix) { if (reversed) pix = _len - pix -1; + else pix += _skip; return PolyBus::getPixelColor(_busPtr, _iType, pix, _colorOrder); } - uint8_t getColorOrder() { + inline uint8_t getColorOrder() { return _colorOrder; } - uint16_t getLength() { - return _len; + inline uint16_t getLength() { + return _len - _skip; } uint8_t getPins(uint8_t* pinArray) { @@ -157,7 +176,15 @@ class BusDigital : public Bus { _colorOrder = colorOrder; } - void reinit() { + inline bool isRgbw() { + return (_type == TYPE_SK6812_RGBW || _type == TYPE_TM1814); + } + + inline uint8_t skippedLeds() { + return _skip; + } + + inline void reinit() { PolyBus::begin(_busPtr, _iType, _pins); } @@ -180,6 +207,7 @@ class BusDigital : public Bus { uint8_t _pins[2] = {255, 255}; uint8_t _iType = I_NONE; uint16_t _len = 0; + uint8_t _skip = 0; void * _busPtr = nullptr; }; @@ -255,7 +283,7 @@ class BusPwm : public Bus { } } - void setBrightness(uint8_t b) { + inline void setBrightness(uint8_t b) { _bri = b; } @@ -265,7 +293,11 @@ class BusPwm : public Bus { return numPins; } - void cleanup() { + bool isRgbw() { + return (_type > TYPE_ONOFF && _type <= TYPE_ANALOG_5CH && _type != TYPE_ANALOG_3CH); + } + + inline void cleanup() { deallocatePins(); } @@ -304,7 +336,7 @@ class BusManager { }; //utility to get the approx. memory usage of a given BusConfig - uint32_t memUsage(BusConfig &bc) { + static uint32_t memUsage(BusConfig &bc) { uint8_t type = bc.type; uint16_t len = bc.count; if (type < 32) { @@ -333,8 +365,7 @@ class BusManager { } else { busses[numBusses] = new BusPwm(bc); } - numBusses++; - return numBusses -1; + return numBusses++; } //do not call this method from system context (network callback) @@ -358,6 +389,7 @@ class BusManager { uint16_t bstart = b->getStart(); if (pix < bstart || pix >= bstart + b->getLength()) continue; busses[i]->setPixelColor(pix - bstart, c); + break; } } @@ -389,14 +421,23 @@ class BusManager { return busses[busNr]; } - uint8_t getNumBusses() { + inline uint8_t getNumBusses() { return numBusses; } - static bool isRgbw(uint8_t type) { - if (type == TYPE_SK6812_RGBW || type == TYPE_TM1814) return true; - if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; - return false; + uint16_t getTotalLength() { + uint16_t len = 0; + for (uint8_t i=0; igetLength(); + return len; + } + + static inline bool isRgbw(uint8_t type) { + return Bus::isRgbw(type); + } + + //Return true if the strip requires a refresh to stay off. + static bool isOffRefreshRequred(uint8_t type) { + return type == TYPE_TM1814; } private: diff --git a/wled00/button.cpp b/wled00/button.cpp index c263a66df6..7d4839c6c4 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -6,105 +6,210 @@ #define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) -void shortPressAction() +void shortPressAction(uint8_t b) { - if (!macroButton) + if (!macroButton[b]) { toggleOnOff(); colorUpdated(NOTIFIER_CALL_MODE_BUTTON); } else { - applyPreset(macroButton); + applyPreset(macroButton[b]); } } -bool isButtonPressed() +bool isButtonPressed(uint8_t i) { - if (btnPin>=0 && digitalRead(btnPin) == LOW) return true; - #ifdef TOUCHPIN - if (touchRead(TOUCHPIN) <= TOUCH_THRESHOLD) return true; - #endif + if (btnPin[i]<0) return false; + switch (buttonType[i]) { + case BTN_TYPE_NONE: + case BTN_TYPE_RESERVED: + break; + case BTN_TYPE_PUSH: + case BTN_TYPE_SWITCH: + if (digitalRead(btnPin[i]) == LOW) return true; + break; + case BTN_TYPE_PUSH_ACT_HIGH: + case BTN_TYPE_SWITCH_ACT_HIGH: + if (digitalRead(btnPin[i]) == HIGH) return true; + break; + case BTN_TYPE_TOUCH: + #ifdef ARDUINO_ARCH_ESP32 + if (touchRead(btnPin[i]) <= touchThreshold) return true; + #endif + break; + } return false; } - -void handleSwitch() +void handleSwitch(uint8_t b) { - if (buttonPressedBefore != isButtonPressed()) { - buttonPressedTime = millis(); - buttonPressedBefore = !buttonPressedBefore; + if (buttonPressedBefore[b] != isButtonPressed(b)) { + buttonPressedTime[b] = millis(); + buttonPressedBefore[b] = !buttonPressedBefore[b]; } - if (buttonLongPressed == buttonPressedBefore) return; + if (buttonLongPressed[b] == buttonPressedBefore[b]) return; - if (millis() - buttonPressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) - if (buttonPressedBefore) { //LOW, falling edge, switch closed - if (macroButton) applyPreset(macroButton); + if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + if (buttonPressedBefore[b]) { //LOW, falling edge, switch closed + if (macroButton[b]) applyPreset(macroButton[b]); else { //turn on if (!bri) {toggleOnOff(); colorUpdated(NOTIFIER_CALL_MODE_BUTTON);} } } else { //HIGH, rising edge, switch opened - if (macroLongPress) applyPreset(macroLongPress); + if (macroLongPress[b]) applyPreset(macroLongPress[b]); else { //turn off if (bri) {toggleOnOff(); colorUpdated(NOTIFIER_CALL_MODE_BUTTON);} } } - buttonLongPressed = buttonPressedBefore; //save the last "long term" switch state + buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state } } +void handleAnalog(uint8_t b) +{ + static uint8_t oldRead[WLED_MAX_BUTTONS]; + #ifdef ESP8266 + uint16_t aRead = analogRead(A0) >> 2; // convert 10bit read to 8bit + #else + uint16_t aRead = analogRead(btnPin[b]) >> 4; // convert 12bit read to 8bit + #endif + // remove noise & reduce frequency of UI updates + aRead &= 0xFC; + + if (oldRead[b] == aRead) return; // no change in reading + oldRead[b] = aRead; + + // if no macro for "short press" and "long press" is defined use brightness control + if (!macroButton[b] && !macroLongPress[b]) { + // if "double press" macro defines which option to change + if (macroDoublePress[b] >= 250) { + // global brightness + if (aRead == 0) { + briLast = bri; + bri = 0; + } else{ + bri = aRead; + } + } else if (macroDoublePress[b] == 249) { + // effect speed + effectSpeed = aRead; + effectChanged = true; + for (uint8_t i = 0; i < strip.getMaxSegments(); i++) { + WS2812FX::Segment& seg = strip.getSegment(i); + if (!seg.isSelected()) continue; + seg.speed = effectSpeed; + } + } else if (macroDoublePress[b] == 248) { + // effect intensity + effectIntensity = aRead; + effectChanged = true; + for (uint8_t i = 0; i < strip.getMaxSegments(); i++) { + WS2812FX::Segment& seg = strip.getSegment(i); + if (!seg.isSelected()) continue; + seg.intensity = effectIntensity; + } + } else if (macroDoublePress[b] == 247) { + // selected palette + effectPalette = map(aRead, 0, 252, 0, strip.getPaletteCount()-1); + effectChanged = true; + for (uint8_t i = 0; i < strip.getMaxSegments(); i++) { + WS2812FX::Segment& seg = strip.getSegment(i); + if (!seg.isSelected()) continue; + seg.palette = effectPalette; + } + } else if (macroDoublePress[b] == 200) { + // primary color, hue, full saturation + colorHStoRGB(aRead*256,255,col); + } else { + // otherwise use "double press" for segment selection + //uint8_t mainSeg = strip.getMainSegmentId(); + WS2812FX::Segment& seg = strip.getSegment(macroDoublePress[b]); + if (aRead == 0) { + seg.setOption(SEG_OPTION_ON, 0); // off + } else { + seg.setOpacity(aRead, macroDoublePress[b]); + seg.setOption(SEG_OPTION_ON, 1); + } + // this will notify clients of update (websockets,mqtt,etc) + //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 + updateInterfaces(NOTIFIER_CALL_MODE_BUTTON); + } + } else { + //TODO: + // we can either trigger a preset depending on the level (between short and long entries) + // or use it for RGBW direct control + } + //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 + colorUpdated(NOTIFIER_CALL_MODE_BUTTON); +} void handleButton() { - if (btnPin<0 || buttonType < BTN_TYPE_PUSH) return; - - - if (buttonType == BTN_TYPE_SWITCH) { //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NO gpio0) - handleSwitch(); return; - } + static unsigned long lastRead = 0UL; + + for (uint8_t b=0; b 250) { // button is not a button but a potentiometer + if (b+1 == WLED_MAX_BUTTONS) lastRead = millis(); + handleAnalog(b); continue; + } - //momentary button logic - if (isButtonPressed()) //pressed - { - if (!buttonPressedBefore) buttonPressedTime = millis(); - buttonPressedBefore = true; + if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_SWITCH_ACT_HIGH) { //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) + handleSwitch(b); continue; + } - if (millis() - buttonPressedTime > 600) //long press + //momentary button logic + if (isButtonPressed(b)) //pressed { - if (!buttonLongPressed) + if (!buttonPressedBefore[b]) buttonPressedTime[b] = millis(); + buttonPressedBefore[b] = true; + + if (millis() - buttonPressedTime[b] > 600) //long press { - if (macroLongPress) {applyPreset(macroLongPress);} - else _setRandomColor(false,true); + if (!buttonLongPressed[b]) + { + if (macroLongPress[b]) {applyPreset(macroLongPress[b]);} + else _setRandomColor(false,true); - buttonLongPressed = true; + buttonLongPressed[b] = true; + } } } - } - else if (!isButtonPressed() && buttonPressedBefore) //released - { - long dur = millis() - buttonPressedTime; - if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore = false; return;} //too short "press", debounce - bool doublePress = buttonWaitTime; - buttonWaitTime = 0; - - if (dur > 6000) //long press + else if (!isButtonPressed(b) && buttonPressedBefore[b]) //released { - WLED::instance().initAP(true); - } - else if (!buttonLongPressed) { //short press - if (macroDoublePress) + long dur = millis() - buttonPressedTime[b]; + if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} //too short "press", debounce + bool doublePress = buttonWaitTime[b]; + buttonWaitTime[b] = 0; + + if (dur > 6000 && b==0) //long press on button 0 { - if (doublePress) applyPreset(macroDoublePress); - else buttonWaitTime = millis(); - } else shortPressAction(); + WLED::instance().initAP(true); + } + else if (!buttonLongPressed[b]) { //short press + if (macroDoublePress[b]) + { + if (doublePress) applyPreset(macroDoublePress[b]); + else buttonWaitTime[b] = millis(); + } else shortPressAction(b); + } + buttonPressedBefore[b] = false; + buttonLongPressed[b] = false; } - buttonPressedBefore = false; - buttonLongPressed = false; - } - if (buttonWaitTime && millis() - buttonWaitTime > 450 && !buttonPressedBefore) - { - buttonWaitTime = 0; - shortPressAction(); + if (buttonWaitTime[b] && millis() - buttonWaitTime[b] > 450 && !buttonPressedBefore[b]) + { + buttonWaitTime[b] = 0; + shortPressAction(b); + } } } @@ -128,7 +233,8 @@ void handleIO() { if (!offMode) { #ifdef ESP8266 - //turn off built-in LED if strip is turned off + // turn off built-in LED if strip is turned off + // this will break digital bus so will need to be reinitialised on On pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); #endif diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 9398dd3ba7..a8aec9aa36 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -12,24 +12,7 @@ void getStringFromJson(char* dest, const char* src, size_t len) { if (src != nullptr) strlcpy(dest, src, len); } -void deserializeConfig() { - bool fromeep = false; - bool success = deserializeConfigSec(); - if (!success) { //if file does not exist, try reading from EEPROM - deEEPSettings(); - fromeep = true; - } - - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - - DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); - - success = readObjectFromFile("/cfg.json", nullptr, &doc); - if (!success) { //if file does not exist, try reading from EEPROM - if (!fromeep) deEEPSettings(); - return; - } - +bool deserializeConfig(JsonObject doc, bool fromFS) { //int rev_major = doc["rev"][0]; // 1 //int rev_minor = doc["rev"][1]; // 0 @@ -79,7 +62,8 @@ void deserializeConfig() { JsonArray ap_ip = ap["ip"]; for (byte i = 0; i < 4; i++) { apIP[i] = ap_ip; - }*/ + } + */ noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted noWifiSleep = !noWifiSleep; @@ -98,85 +82,114 @@ void deserializeConfig() { CJSON(strip.rgbwMode, hw_led[F("rgbwm")]); JsonArray ins = hw_led["ins"]; - uint8_t s = 0; //bus iterator - strip.isRgbw = false; - busses.removeAll(); - uint32_t mem = 0; - for (JsonObject elm : ins) { - if (s >= WLED_MAX_BUSSES) break; - uint8_t pins[5] = {255, 255, 255, 255, 255}; - JsonArray pinArr = elm[F("pin")]; - if (pinArr.size() == 0) continue; - pins[0] = pinArr[0]; - uint8_t i = 0; - for (int p : pinArr) { - pins[i] = p; - i++; - if (i>4) break; + if (fromFS || !ins.isNull()) { + uint8_t s = 0; //bus iterator + strip.isRgbw = false; + busses.removeAll(); + uint32_t mem = 0; + for (JsonObject elm : ins) { + if (s >= WLED_MAX_BUSSES) break; + uint8_t pins[5] = {255, 255, 255, 255, 255}; + JsonArray pinArr = elm["pin"]; + if (pinArr.size() == 0) continue; + pins[0] = pinArr[0]; + uint8_t i = 0; + for (int p : pinArr) { + pins[i++] = p; + if (i>4) break; + } + + uint16_t length = elm[F("len")]; + if (length==0) continue; + uint8_t colorOrder = (int)elm[F("order")]; + //only use skip from the first strip (this shouldn't have been in ins obj. but remains here for compatibility) + uint8_t skipFirst = elm[F("skip")]; + uint16_t start = elm[F("start")] | 0; + if (start >= ledCount) continue; + //limit length of strip if it would exceed total configured LEDs + if (start + length > ledCount) length = ledCount - start; + uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; + bool reversed = elm["rev"]; + //RGBW mode is enabled if at least one of the strips is RGBW + strip.isRgbw = (strip.isRgbw || BusManager::isRgbw(ledType)); + //refresh is required to remain off if at least one of the strips requires the refresh. + strip.isOffRefreshRequred |= BusManager::isOffRefreshRequred(ledType); + s++; + BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); + mem += busses.memUsage(bc); + if (mem <= MAX_LED_MEMORY) busses.add(bc); } - - uint16_t length = elm[F("len")]; - if (length==0) continue; - uint8_t colorOrder = (int)elm[F("order")]; - //only use skip from the first strip (this shouldn't have been in ins obj. but remains here for compatibility) - if (s==0) skipFirstLed = elm[F("skip")]; - uint16_t start = elm[F("start")] | 0; - if (start >= ledCount) continue; - //limit length of strip if it would exceed total configured LEDs - if (start + length > ledCount) length = ledCount - start; - uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; - bool reversed = elm["rev"]; - //RGBW mode is enabled if at least one of the strips is RGBW - strip.isRgbw = (strip.isRgbw || BusManager::isRgbw(ledType)); - s++; - BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed); - mem += busses.memUsage(bc); - if (mem <= MAX_LED_MEMORY) busses.add(bc); + strip.finalizeInit(ledCount); } - strip.finalizeInit(ledCount, skipFirstLed); if (hw_led["rev"]) busses.getBus(0)->reversed = true; //set 0.11 global reversed setting for first bus - JsonObject hw_btn_ins_0 = hw[F("btn")][F("ins")][0]; - CJSON(buttonType, hw_btn_ins_0["type"]); - int hw_btn_pin = hw_btn_ins_0[F("pin")][0]; - if (pinManager.allocatePin(hw_btn_pin,false)) { - btnPin = hw_btn_pin; - pinMode(btnPin, INPUT_PULLUP); + // read multiple button configuration + JsonArray hw_btn_ins = hw[F("btn")][F("ins")]; + if (!hw_btn_ins.isNull()) { + uint8_t s = 0; + for (JsonObject btn : hw_btn_ins) { + CJSON(buttonType[s], btn["type"]); + int8_t pin = btn[F("pin")][0] | -1; + if (pin > -1) { + if (pinManager.allocatePin(pin,false)) { + btnPin[s] = pin; + pinMode(btnPin[s], INPUT_PULLUP); + } else { + btnPin[s] = -1; + } + } + JsonArray hw_btn_ins_0_macros = btn[F("macros")]; + CJSON(macroButton[s], hw_btn_ins_0_macros[0]); + CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]); + CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]); + if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached + } + // clear remaining buttons + for (; s -2) { + if (pinManager.allocatePin(hw_ir_pin,false)) { + irPin = hw_ir_pin; + } else { + irPin = -1; + } } - #endif CJSON(irEnabled, hw["ir"]["type"]); JsonObject relay = hw[F("relay")]; - - int hw_relay_pin = relay["pin"]; - if (pinManager.allocatePin(hw_relay_pin,true)) { - rlyPin = hw_relay_pin; - pinMode(rlyPin, OUTPUT); - } else { - rlyPin = -1; + int hw_relay_pin = relay["pin"] | -2; + if (hw_relay_pin > -2) { + if (pinManager.allocatePin(hw_relay_pin,true)) { + rlyPin = hw_relay_pin; + pinMode(rlyPin, OUTPUT); + } else { + rlyPin = -1; + } } if (relay.containsKey("rev")) { rlyMde = !relay["rev"]; } - //int hw_status_pin = hw[F("status")][F("pin")]; // -1 + //int hw_status_pin = hw[F("status")]["pin"]; // -1 JsonObject light = doc[F("light")]; CJSON(briMultiplier, light[F("scale-bri")]); @@ -191,14 +204,15 @@ void deserializeConfig() { JsonObject light_tr = light[F("tr")]; CJSON(fadeTransition, light_tr[F("mode")]); - int tdd = light_tr[F("dur")] | -1; + int tdd = light_tr["dur"] | -1; if (tdd >= 0) transitionDelayDefault = tdd * 100; - CJSON(strip.paletteFade, light_tr[F("pal")]); + CJSON(strip.paletteFade, light_tr["pal"]); JsonObject light_nl = light["nl"]; CJSON(nightlightMode, light_nl[F("mode")]); + byte prev = nightlightDelayMinsDefault; CJSON(nightlightDelayMinsDefault, light_nl[F("dur")]); - nightlightDelayMins = nightlightDelayMinsDefault; + if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault; CJSON(nightlightTargetBri, light_nl[F("tbri")]); CJSON(macroNl, light_nl[F("macro")]); @@ -214,7 +228,7 @@ void deserializeConfig() { CJSON(presetCycleMin, def_cy[F("range")][0]); CJSON(presetCycleMax, def_cy[F("range")][1]); - tdd = def_cy[F("dur")] | -1; + tdd = def_cy["dur"] | -1; if (tdd > 0) presetCycleTime = tdd; JsonObject interfaces = doc["if"]; @@ -227,11 +241,13 @@ void deserializeConfig() { CJSON(receiveNotificationBrightness, if_sync_recv["bri"]); CJSON(receiveNotificationColor, if_sync_recv["col"]); CJSON(receiveNotificationEffects, if_sync_recv[F("fx")]); + //! following line might be a problem if called after boot receiveNotifications = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects); JsonObject if_sync_send = if_sync["send"]; + prev = notifyDirectDefault; CJSON(notifyDirectDefault, if_sync_send[F("dir")]); - notifyDirect = notifyDirectDefault; + if (notifyDirectDefault != prev) notifyDirect = notifyDirectDefault; CJSON(notifyButton, if_sync_send[F("btn")]); CJSON(notifyAlexa, if_sync_send[F("va")]); CJSON(notifyHue, if_sync_send[F("hue")]); @@ -264,6 +280,7 @@ void deserializeConfig() { CJSON(macroAlexaOn, interfaces[F("va")][F("macros")][0]); CJSON(macroAlexaOff, interfaces[F("va")][F("macros")][1]); +#ifndef WLED_DISABLE_BLYNK const char* apikey = interfaces["blynk"][F("token")] | "Hidden"; tdd = strnlen(apikey, 36); if (tdd > 20 || tdd == 0) @@ -272,7 +289,9 @@ void deserializeConfig() { JsonObject if_blynk = interfaces["blynk"]; getStringFromJson(blynkHost, if_blynk[F("host")], 33); CJSON(blynkPort, if_blynk["port"]); +#endif +#ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; CJSON(mqttEnabled, if_mqtt["en"]); getStringFromJson(mqttServer, if_mqtt[F("broker")], 33); @@ -283,7 +302,9 @@ void deserializeConfig() { getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], 33); // "wled/test" getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], 33); // "" +#endif +#ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces[F("hue")]; CJSON(huePollingEnabled, if_hue["en"]); CJSON(huePollLightId, if_hue["id"]); @@ -299,6 +320,7 @@ void deserializeConfig() { for (byte i = 0; i < 4; i++) CJSON(hueIP[i], if_hue_ip[i]); +#endif JsonObject if_ntp = interfaces[F("ntp")]; CJSON(ntpEnabled, if_ntp["en"]); @@ -310,11 +332,12 @@ void deserializeConfig() { CJSON(latitude, if_ntp[F("lt")]); JsonObject ol = doc[F("ol")]; + prev = overlayDefault; CJSON(overlayDefault ,ol[F("clock")]); // 0 CJSON(countdownMode, ol[F("cntdwn")]); - overlayCurrent = overlayDefault; + if (prev != overlayDefault) overlayCurrent = overlayDefault; - CJSON(overlayMin, ol[F("min")]); + CJSON(overlayMin, ol["min"]); CJSON(overlayMax, ol[F("max")]); CJSON(analogClock12pixel, ol[F("o12pix")]); CJSON(analogClock5MinuteMarks, ol[F("o5m")]); @@ -342,7 +365,7 @@ void deserializeConfig() { if (it > 9) break; if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset CJSON(timerHours[it], timer[F("hour")]); - CJSON(timerMinutes[it], timer[F("min")]); + CJSON(timerMinutes[it], timer["min"]); CJSON(timerMacro[it], timer[F("macro")]); byte dowPrev = timerWeekday[it]; @@ -388,8 +411,34 @@ void deserializeConfig() { } #endif + DEBUG_PRINTLN(F("Starting usermod config.")); JsonObject usermods_settings = doc["um"]; - usermods.readFromConfig(usermods_settings); + if (!usermods_settings.isNull()) usermods.readFromConfig(usermods_settings); + + if (fromFS) return false; + doReboot = doc[F("rb")] | doReboot; + return (doc["sv"] | true); +} + +void deserializeConfigFromFS() { + bool fromeep = false; + bool success = deserializeConfigSec(); + if (!success) { //if file does not exist, try reading from EEPROM + deEEPSettings(); + fromeep = true; + } + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + + DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); + + success = readObjectFromFile("/cfg.json", nullptr, &doc); + if (!success) { //if file does not exist, try reading from EEPROM + if (!fromeep) deEEPSettings(); + return; + } + + deserializeConfig(doc.as(), true); } void serializeConfig() { @@ -472,31 +521,42 @@ void serializeConfig() { for (uint8_t i = 0; i < nPins; i++) ins_pin.add(pins[i]); ins[F("order")] = bus->getColorOrder(); ins["rev"] = bus->reversed; - ins[F("skip")] = (skipFirstLed && s == 0) ? 1 : 0; + ins[F("skip")] = bus->skippedLeds(); ins["type"] = bus->getType(); } + // button(s) JsonObject hw_btn = hw.createNestedObject("btn"); - + hw_btn["max"] = WLED_MAX_BUTTONS; // just information about max number of buttons (not actually used) JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); - // button BTNPIN + // there is always at least one button JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject(); - hw_btn_ins_0["type"] = buttonType; - + hw_btn_ins_0["type"] = buttonType[0]; JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin"); - hw_btn_ins_0_pin.add(btnPin); - + hw_btn_ins_0_pin.add(btnPin[0]); JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros"); - hw_btn_ins_0_macros.add(macroButton); - hw_btn_ins_0_macros.add(macroLongPress); - hw_btn_ins_0_macros.add(macroDoublePress); + hw_btn_ins_0_macros.add(macroButton[0]); + hw_btn_ins_0_macros.add(macroLongPress[0]); + hw_btn_ins_0_macros.add(macroDoublePress[0]); + + // additional buttons + for (uint8_t i=1; i> 1; } @@ -672,7 +739,6 @@ void serializeConfig() { for (byte i = 0; i < 15; i++) dmx_fixmap.add(DMXFixtureMap[i]); #endif - //} JsonObject usermods_settings = doc.createNestedObject("um"); usermods.addToConfig(usermods_settings); @@ -699,15 +765,21 @@ bool deserializeConfigSec() { JsonObject interfaces = doc["if"]; +#ifndef WLED_DISABLE_BLYNK const char* apikey = interfaces["blynk"][F("token")] | "Hidden"; int tdd = strnlen(apikey, 36); if (tdd > 20 || tdd == 0) getStringFromJson(blynkApiKey, apikey, 36); +#endif +#ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; getStringFromJson(mqttPass, if_mqtt["psk"], 41); +#endif +#ifndef WLED_DISABLE_HUESYNC getStringFromJson(hueApiKey, interfaces[F("hue")][F("key")], 47); +#endif JsonObject ota = doc["ota"]; getStringFromJson(otaPass, ota[F("pwd")], 33); @@ -734,12 +806,18 @@ void serializeConfigSec() { ap["psk"] = apPass; JsonObject interfaces = doc.createNestedObject("if"); +#ifndef WLED_DISABLE_BLYNK JsonObject if_blynk = interfaces.createNestedObject("blynk"); if_blynk[F("token")] = blynkApiKey; +#endif +#ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["psk"] = mqttPass; +#endif +#ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces.createNestedObject("hue"); if_hue[F("key")] = hueApiKey; +#endif JsonObject ota = doc.createNestedObject("ota"); ota[F("pwd")] = otaPass; diff --git a/wled00/const.h b/wled00/const.h index 850a0228db..85f8c25fbc 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -11,43 +11,68 @@ #define DEFAULT_OTA_PASS "wledota" //increase if you need more -#define WLED_MAX_USERMODS 4 +#ifndef WLED_MAX_USERMODS + #ifdef ESP8266 + #define WLED_MAX_USERMODS 4 + #else + #define WLED_MAX_USERMODS 6 + #endif +#endif -#ifdef ESP8266 -#define WLED_MAX_BUSSES 3 -#else -#define WLED_MAX_BUSSES 10 +#ifndef WLED_MAX_BUSSES + #ifdef ESP8266 + #define WLED_MAX_BUSSES 3 + #else + #ifdef CONFIG_IDF_TARGET_ESP32S2 + #define WLED_MAX_BUSSES 5 + #else + #define WLED_MAX_BUSSES 10 + #endif + #endif +#endif + +#ifndef WLED_MAX_BUTTONS + #ifdef ESP8266 + #define WLED_MAX_BUTTONS 2 + #else + #define WLED_MAX_BUTTONS 4 + #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 -#define USERMOD_ID_EXAMPLE 2 //Usermod "usermod_v2_example.h" -#define USERMOD_ID_TEMPERATURE 3 //Usermod "usermod_temperature.h" -#define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h" -#define USERMOD_ID_PIRSWITCH 5 //Usermod "usermod_PIR_sensor_switch.h" -#define USERMOD_ID_IMU 6 //Usermod "usermod_mpu6050_imu.h" -#define USERMOD_ID_FOUR_LINE_DISP 7 //Usermod "usermod_v2_four_line_display.h -#define USERMOD_ID_ROTARY_ENC_UI 8 //Usermod "usermod_v2_rotary_encoder_ui.h" -#define USERMOD_ID_AUTO_SAVE 9 //Usermod "usermod_v2_auto_save.h" -#define USERMOD_ID_DHT 10 //Usermod "usermod_dht.h" -#define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h" -#define USERMOD_ID_VL53L0X 12 //Usermod "usermod_vl53l0x_gestures.h" +#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 +#define USERMOD_ID_EXAMPLE 2 //Usermod "usermod_v2_example.h" +#define USERMOD_ID_TEMPERATURE 3 //Usermod "usermod_temperature.h" +#define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h" +#define USERMOD_ID_PIRSWITCH 5 //Usermod "usermod_PIR_sensor_switch.h" +#define USERMOD_ID_IMU 6 //Usermod "usermod_mpu6050_imu.h" +#define USERMOD_ID_FOUR_LINE_DISP 7 //Usermod "usermod_v2_four_line_display.h +#define USERMOD_ID_ROTARY_ENC_UI 8 //Usermod "usermod_v2_rotary_encoder_ui.h" +#define USERMOD_ID_AUTO_SAVE 9 //Usermod "usermod_v2_auto_save.h" +#define USERMOD_ID_DHT 10 //Usermod "usermod_dht.h" +#define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h" +#define USERMOD_ID_VL53L0X 12 //Usermod "usermod_vl53l0x_gestures.h" +#define USERMOD_ID_MULTI_RELAY 13 //Usermod "usermod_multi_relay.h" +#define USERMOD_ID_ANIMATED_STAIRCASE 14 //Usermod "Animated_Staircase.h" +#define USERMOD_ID_RTC 15 //Usermod "usermod_rtc.h" +#define USERMOD_ID_ELEKSTUBE_IPS 16 //Usermod "usermod_elekstube_ips.h" +#define USERMOD_ID_SN_PHOTORESISTOR 17 //Usermod "usermod_sn_photoresistor.h" //Access point behavior -#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot -#define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost) -#define AP_BEHAVIOR_ALWAYS 2 //Always open -#define AP_BEHAVIOR_BUTTON_ONLY 3 //Only when button pressed for 6 sec +#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot +#define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost) +#define AP_BEHAVIOR_ALWAYS 2 //Always open +#define AP_BEHAVIOR_BUTTON_ONLY 3 //Only when button pressed for 6 sec -//Notifier callMode -#define NOTIFIER_CALL_MODE_INIT 0 //no updates on init, can be used to disable updates +//Notifier callMode +#define NOTIFIER_CALL_MODE_INIT 0 //no updates on init, can be used to disable updates #define NOTIFIER_CALL_MODE_DIRECT_CHANGE 1 #define NOTIFIER_CALL_MODE_BUTTON 2 #define NOTIFIER_CALL_MODE_NOTIFICATION 3 #define NOTIFIER_CALL_MODE_NIGHTLIGHT 4 #define NOTIFIER_CALL_MODE_NO_NOTIFY 5 -#define NOTIFIER_CALL_MODE_FX_CHANGED 6 //no longer used +#define NOTIFIER_CALL_MODE_FX_CHANGED 6 //no longer used #define NOTIFIER_CALL_MODE_HUE 7 #define NOTIFIER_CALL_MODE_PRESET_CYCLE 8 #define NOTIFIER_CALL_MODE_BLYNK 9 @@ -57,7 +82,7 @@ #define RGBW_MODE_MANUAL_ONLY 0 //No automatic white channel calculation. Manual white channel slider #define RGBW_MODE_AUTO_BRIGHTER 1 //New algorithm. Adds as much white as the darkest RGBW channel #define RGBW_MODE_AUTO_ACCURATE 2 //New algorithm. Adds as much white as the darkest RGBW channel and subtracts this amount from each RGB channel -#define RGBW_MODE_DUAL 3 //Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0) +#define RGBW_MODE_DUAL 3 //Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0) #define RGBW_MODE_LEGACY 4 //Old floating algorithm. Too slow for realtime and palette support //realtime modes @@ -116,10 +141,10 @@ #define TYPE_LPD8806 52 #define TYPE_P9813 53 -#define IS_DIGITAL(t) (t & 0x10) //digital are 16-31 and 48-63 -#define IS_PWM(t) (t > 40 && t < 46) -#define NUM_PWM_PINS(t) (t - 40) //for analog PWM 41-45 only -#define IS_2PIN(t) (t > 47) +#define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63 +#define IS_PWM(t) ((t) > 40 && (t) < 46) +#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only +#define IS_2PIN(t) ((t) > 47) //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut @@ -134,9 +159,11 @@ #define BTN_TYPE_NONE 0 #define BTN_TYPE_RESERVED 1 #define BTN_TYPE_PUSH 2 -#define BTN_TYPE_PUSH_ACT_HIGH 3 //not implemented +#define BTN_TYPE_PUSH_ACT_HIGH 3 #define BTN_TYPE_SWITCH 4 -#define BTN_TYPE_SWITCH_ACT_HIGH 5 //not implemented +#define BTN_TYPE_SWITCH_ACT_HIGH 5 +#define BTN_TYPE_TOUCH 6 +#define BTN_TYPE_ANALOG 7 //Ethernet board types #define WLED_NUM_ETH_TYPES 5 @@ -165,6 +192,9 @@ #define SEG_OPTION_FREEZE 5 //Segment contents will not be refreshed #define SEG_OPTION_TRANSITIONAL 7 +//Playlist option byte +#define PL_OPTION_SHUFFLE 0x01 + // WLED Error modes #define ERR_NONE 0 // All good :) #define ERR_EEP_COMMIT 2 // Could not commit to EEPROM (wrong flash layout?) @@ -189,7 +219,7 @@ // maximum number of LEDs - more than 1500 LEDs (or 500 DMA "LEDPIN 3" driven ones) will cause a low memory condition on ESP8266 #ifndef MAX_LEDS #ifdef ESP8266 -#define MAX_LEDS 8192 //rely on memory limit to limit this to 1600 LEDs +#define MAX_LEDS 1664 // can't rely on memory limit to limit this to 1600 LEDs #else #define MAX_LEDS 8192 #endif @@ -210,7 +240,11 @@ // string temp buffer (now stored in stack locally) #define OMAX 2048 -#define E131_MAX_UNIVERSE_COUNT 9 +#ifdef WLED_USE_ETHERNET +#define E131_MAX_UNIVERSE_COUNT 20 +#else +#define E131_MAX_UNIVERSE_COUNT 10 +#endif #define ABL_MILLIAMPS_DEFAULT 850 // auto lower brightness to stay close to milliampere limit @@ -229,7 +263,7 @@ #ifdef ESP8266 #define JSON_BUFFER_SIZE 9216 #else - #define JSON_BUFFER_SIZE 16384 + #define JSON_BUFFER_SIZE 20480 #endif // Maximum size of node map (list of other WLED instances) @@ -241,7 +275,11 @@ //this is merely a default now and can be changed at runtime #ifndef LEDPIN -#define LEDPIN 2 +#ifdef ESP8266 + #define LEDPIN 2 // GPIO2 (D4) on Wemod D1 mini compatible boards +#else + #define LEDPIN 16 // alligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards +#endif #endif #ifdef WLED_ENABLE_DMX diff --git a/wled00/data/404.htm b/wled00/data/404.htm index 1b7c95fc0f..87244a94ff 100644 --- a/wled00/data/404.htm +++ b/wled00/data/404.htm @@ -39,7 +39,7 @@ - +

404 Not Found

Akemi does not know where you are headed...

diff --git a/wled00/data/index.js b/wled00/data/index.js index 6501a5f412..2499c1fc83 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -898,8 +898,11 @@ function cmpP(a, b) { } var jsonTimeout; +var reqsLegal = false; + function requestJson(command, rinfo = true, verbose = true) { d.getElementById('connind').style.backgroundColor = "#a90"; + if (command && !reqsLegal) return; //stop post requests from chrome onchange event on page restore lastUpdate = new Date(); if (!jsonTimeout) jsonTimeout = setTimeout(showErrorToast, 3000); var req = null; @@ -959,6 +962,7 @@ function requestJson(command, rinfo = true, verbose = true) { populateEffects(json.effects); populatePalettes(json.palettes); + reqsLegal = true; } var info = json.info; @@ -1036,7 +1040,10 @@ function requestJson(command, rinfo = true, verbose = true) { d.getElementById('sliderIntensity').value = i.ix; // Effects - e1.querySelector(`input[name="fx"][value="${i.fx}"]`).checked = true; + var selFx = e1.querySelector(`input[name="fx"][value="${i.fx}"]`); + if (selFx) selFx.checked = true; + else location.reload(); //effect list is gone (e.g. if restoring tab). Reload. + var selElement = e1.querySelector('.selected'); if (selElement) { selElement.classList.remove('selected') diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index 4d7eaaed21..6cb09f8308 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -10,18 +10,17 @@ margin: 0; } html { - --h: 11.55vh; + --h: 10.2vh; } button { background: #333; color: #fff; font-family: Verdana, Helvetica, sans-serif; - border: 0.3ch solid #333; - display: inline-block; - font-size: 8vmin; + border: 1px solid #333; + font-size: 6vmin; height: var(--h); width: 95%; - margin-top: 2.4vh; + margin-top: 2vh; } -
+

LED & Hardware setup

- Total LED count:
- Recommended power supply for brightest white:
- ?
-
-
- Enable automatic brightness limiter:
-
- Maximum Current: mA
-

Hardware setup

LED outputs:
@@ -302,11 +345,22 @@

Hardware setup

- Button pin:  ×
- IR pin:  ×
- Relay pin:  ×
- Active high +

+
+ Touch threshold:
+ IR pin:   ×
+ IR info
+ Relay pin: Invert  ×
+

Defaults

Turn LEDs on after power up/reset:
Default brightness: (0-255)

@@ -338,7 +392,6 @@

Advanced


- Skip first LED:
Auto-calculate white channel from RGB:
- - - -
-Infrared remote: -
-IR info

WLED Broadcast

UDP Port:
2nd Port:
-Receive Brightness, Color, and Effects
+Receive: Brightness, Color, and Effects
Send notifications on direct change:
Send notifications on button press or IR:
Send Alexa notifications:
diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index fdb232b389..e436ee4ed8 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -6,48 +6,52 @@ Time Settings + + + + +
+
+
+ +
+
+

Usermod Setup

+
Loading settings...
+
+ + + + \ No newline at end of file diff --git a/wled00/data/update.htm b/wled00/data/update.htm index f3a0a22113..16dede5cbe 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -43,7 +43,7 @@

WLED Software Update

Installed version: ##VERSION##
Download the latest binary:
-
+

Updating...
Please do not close or refresh the page :)
diff --git a/wled00/data/welcome.htm b/wled00/data/welcome.htm index f0e1b463d0..1b2bb60f95 100644 --- a/wled00/data/welcome.htm +++ b/wled00/data/welcome.htm @@ -31,11 +31,11 @@ } img { - width: 999px; - max-width: 85%; + width: 950px; + max-width: 82%; image-rendering: pixelated; image-rendering: crisp-edges; - margin: 25px 0 -10px 0; + margin: 4vh 0 0 0; animation: fi 1s; } @@ -50,7 +50,7 @@ - +

Welcome to WLED!

Thank you for installing my application!

diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index c4405d144a..e624e5248f 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -20,13 +20,14 @@ void handleBlynk(); void updateBlynk(); //button.cpp -void shortPressAction(); -bool isButtonPressed(); +void shortPressAction(uint8_t b=0); +bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); //cfg.cpp -void deserializeConfig(); +bool deserializeConfig(JsonObject doc, bool fromFS = false); +void deserializeConfigFromFS(); bool deserializeConfigSec(); void serializeConfig(); void serializeConfigSec(); @@ -96,8 +97,8 @@ void handleIR(); #include "src/dependencies/json/AsyncJson-v6.h" #include "FX.h" -void deserializeSegment(JsonObject elem, byte it); -bool deserializeState(JsonObject root); +void deserializeSegment(JsonObject elem, byte it, byte presetId = 0); +bool deserializeState(JsonObject root, byte presetId = 0); void serializeSegment(JsonObject& root, WS2812FX::Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true); void serializeInfo(JsonObject root); @@ -126,6 +127,7 @@ bool initMqtt(); void publishMqtt(); //ntp.cpp +void handleTime(); void handleNetworkTime(); void sendNTPPacket(); bool checkNTPResponse(); @@ -136,6 +138,7 @@ void setCountdown(); byte weekdayMondayFirst(); void checkTimers(); void calculateSunriseAndSunset(); +void setTimeFromAPI(uint32_t timein); //overlay.cpp void initCronixie(); @@ -150,8 +153,9 @@ void _overlayCronixie(); void _drawOverlayCronixie(); //playlist.cpp +void shufflePlaylist(); void unloadPlaylist(); -void loadPlaylist(JsonObject playlistObject); +void loadPlaylist(JsonObject playlistObject, byte presetId = 0); void handlePlaylist(); //presets.cpp @@ -186,6 +190,8 @@ class Usermod { virtual void readFromJsonState(JsonObject& obj) {} virtual void addToConfig(JsonObject& obj) {} virtual void readFromConfig(JsonObject& obj) {} + virtual void onMqttConnect(bool sessionPresent) {} + virtual bool onMqttMessage(char* topic, char* payload) { return false; } virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} }; @@ -206,7 +212,8 @@ class UsermodManager { void addToConfig(JsonObject& obj); void readFromConfig(JsonObject& obj); - + void onMqttConnect(bool sessionPresent); + bool onMqttMessage(char* topic, char* payload); bool add(Usermod* um); Usermod* lookup(uint16_t mod_id); byte getModCount(); diff --git a/wled00/file.cpp b/wled00/file.cpp index e3aac3ae00..e6c0d2cd8e 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -55,13 +55,12 @@ bool bufferedFind(const char *target, bool fromStart = true) { size_t targetLen = strlen(target); size_t index = 0; - uint16_t bufsize = 0, count = 0; byte buf[FS_BUFSIZE]; if (fromStart) f.seek(0); while (f.position() < f.size() -1) { - bufsize = f.read(buf, FS_BUFSIZE); - count = 0; + uint16_t bufsize = f.read(buf, FS_BUFSIZE); + uint16_t count = 0; while (count < bufsize) { if(buf[count] != target[index]) index = 0; // reset index if any char does not match @@ -97,13 +96,12 @@ bool bufferedFindSpace(uint16_t targetLen, bool fromStart = true) { if (!f || !f.size()) return false; uint16_t index = 0; - uint16_t bufsize = 0, count = 0; byte buf[FS_BUFSIZE]; if (fromStart) f.seek(0); while (f.position() < f.size() -1) { - bufsize = f.read(buf, FS_BUFSIZE); - count = 0; + uint16_t bufsize = f.read(buf, FS_BUFSIZE); + uint16_t count = 0; while (count < bufsize) { if(buf[count] == ' ') { @@ -140,13 +138,12 @@ bool bufferedFindObjectEnd() { if (!f || !f.size()) return false; uint16_t objDepth = 0; //num of '{' minus num of '}'. return once 0 - uint16_t bufsize = 0, count = 0; //size_t start = f.position(); byte buf[FS_BUFSIZE]; while (f.position() < f.size() -1) { - bufsize = f.read(buf, FS_BUFSIZE); - count = 0; + uint16_t bufsize = f.read(buf, FS_BUFSIZE); + uint16_t count = 0; while (count < bufsize) { if (buf[count] == '{') objDepth++; diff --git a/wled00/html_other.h b/wled00/html_other.h index ad55de5903..19d6667e75 100644 --- a/wled00/html_other.h +++ b/wled00/html_other.h @@ -45,19 +45,19 @@ action="/update" id="uf" enctype="multipart/form-data" onsubmit="U()"> Installed version: 0.12.1-b1
Download the latest binary: -

-
Updating... -
Please do not close or refresh the page :)
)====="; +


Updating...
+Please do not close or refresh the page :)
)====="; // Autogenerated from wled00/data/welcome.htm, do not edit!! const char PAGE_welcome[] PROGMEM = R"=====(Welcome! +src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG0AAAAfCAMAAADazLOuAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABLUExURQAAAAB81gCU/zKq///mo7sWMN8bO+ZIYtZaAP9rAP+HMsCiG+TAIOnMS0KqNU7KPnLUZOrq6v///4CAgGhoaL+/v6CgoExMTAAAAAlm4O8AAAAZdFJOU////////////////////////////////wABNAq3AAAACXBIWXMAAA7DAAAOwwHHb6hkAAACN0lEQVRIS73VjVLCMBAEYIr8CYKkrdj3f1J37zaXFCpTO+piaDgbPq9px9VQ0qyrvKj4q6m0Zr1h+M7xF1zRmnWzqV9/0d2jttGotO1uv9dUObwej5oqp7fzWVPl8n69aprzoOUUbbvdIbV3OLwitXc6vSG1d7m8I3feSEN0j2CeNbOY4MxigjOLCc4sZsTV2l1cCyy4wIILLLjAxtykltq2rbTU+qi01N5rXNO2leaFORoija2l5MM5a02ac9Ya16Sk5tgaPrUpjZub0BL6YqSxKwbH77XUUmSkJXSl8QtaMuyJhq5maL5nTKVpZC13VmtMpTFT2g4vJjTuGfMzzXftiUZnhdtgb1xofvypRon5TjNnxYN9zJo6K5ruSIzQtGuVZn0x91rKvdHBvm39E7SyZ4y06Gz8BDBFKzsXmhcwyfsGZ9VpbhoiCinaxPNmGWmWWrNU2jB0q6HvOhN1JUtCixQtp2g51ZVUXIPS2RMAD++T2nY/DrDjOMDO4wC7jmNYj3d73nrXug8Yt9uNB8xNU1cKNXWlUFNXCjV1pZhGTE83m2vWfYf/NGj4Bg1zu5JD3/MnH5ZWfLOksbmGWGjgXMN5/C2GXYGFFW9Nmtle6Xut0Gm+JsayCj8z0nhjGvYJzVf4aSzmNYsr+u7Q2JIdoX3YOQjOslmsW1jJ3120nE9gfo79hTaNdcsqVR610lvO47pllae9ReZ805zKo2a3iaY5c75pTmVCA6dJ5H7N0sr/asPwBehb7ifEhusRAAAAAElFTkSuQmCC">

Welcome to WLED!

Thank you for installing my application!

Next steps:

Connect the module to your local WiFi here!
)====="; diff --git a/wled00/html_settings.h b/wled00/html_settings.h index 5d24d9ac95..c7e387af11 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -12,7 +12,7 @@ const char PAGE_settingsCss[] PROGMEM = R"=====(
%DMXMENU%
+ action="/settings/um">
)====="; @@ -72,19 +73,18 @@ Do not enable if WiFi is working correctly, increases power consumption.
LED Settings")); } From 826ac8bf7dd920390a59aec49e5e94e3a496ffe2 Mon Sep 17 00:00:00 2001 From: lodesmets Date: Sat, 19 Jun 2021 16:58:26 +0200 Subject: [PATCH 3/3] - ran npm build --- wled00/html_settings.h | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/wled00/html_settings.h b/wled00/html_settings.h index 4405fb2cbc..f47eaade91 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -300,15 +300,7 @@ type="submit">Save)====="; // Autogenerated from wled00/data/settings_time.htm, do not edit!! const char PAGE_settings_time[] PROGMEM = R"=====( Time Settings