diff --git a/CHANGELOG.md b/CHANGELOG.md index baa1e307..4627acce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ### Builds after release 0.12.0 +#### Build 2110060 + +- Added virtual network DDP busses (PR #2245) +- Allow playlist as end preset in playlist +- Improved bus start field UX +- Pin reservations improvements (PR #2214) + #### Build 2109220 - Version bump to 0.13.0-b3 "Toki" diff --git a/usermods/BH1750_v2/platformio_override.ini b/usermods/BH1750_v2/platformio_override.ini new file mode 100644 index 00000000..7b981e50 --- /dev/null +++ b/usermods/BH1750_v2/platformio_override.ini @@ -0,0 +1,16 @@ +; Options +; ------- +; USERMOD_BH1750 - define this to have this user mod included wled00\usermods_list.cpp +; USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - the max number of milliseconds between measurements, defaults to 10000ms +; USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL - the min number of milliseconds between measurements, defaults to 500ms +; USERMOD_BH1750_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 10 seconds +; USERMOD_BH1750_OFFSET_VALUE - the offset value to report on, defaults to 1 +; +[env:usermod_BH1750_d1_mini] +extends = env:d1_mini +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_BH1750 +lib_deps = + ${env.lib_deps} + claws/BH1750 @ ^1.2.0 diff --git a/usermods/BH1750_v2/readme.md b/usermods/BH1750_v2/readme.md new file mode 100644 index 00000000..9496dc02 --- /dev/null +++ b/usermods/BH1750_v2/readme.md @@ -0,0 +1,24 @@ +# BH1750 usermod + +This usermod will read from an ambient light sensor like the BH1750 sensor. +The luminance is displayed both in the Info section of the web UI as well as published to the `/luminance` MQTT topic if enabled. + +## Installation + +Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_BH1750` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms +* `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms +* `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10 seconds +* `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 + +All parameters can be configured at runtime using Usermods settings page. + +### 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:usermod_BH1750_d1_mini`. + +## Change Log diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h new file mode 100644 index 00000000..fb0b1c5a --- /dev/null +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -0,0 +1,177 @@ +#pragma once + +#include "wled.h" +#include +#include + +// the max frequency to check photoresistor, 10 seconds +#ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 +#endif + +// the min frequency to check photoresistor, 500 ms +#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 +#endif + +// how many seconds after boot to take first measurement, 10 seconds +#ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT +#define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 +#endif + +// only report if differance grater than offset value +#ifndef USERMOD_BH1750_OFFSET_VALUE +#define USERMOD_BH1750_OFFSET_VALUE 1 +#endif + +class Usermod_BH1750 : public Usermod +{ +private: + int8_t offset = USERMOD_BH1750_OFFSET_VALUE; + + unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; + unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; + unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + // flag to indicate we have finished the first readLightLevel call + // allows this library to report to the user how long until the first + // measurement + bool getLuminanceComplete = false; + + // flag set at startup + bool disabled = false; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _maxReadInterval[]; + static const char _minReadInterval[]; + static const char _offset[]; + + BH1750 lightMeter; + float lastLux = -1000; + + bool checkBoundSensor(float newValue, float prevValue, float maxDiff) + { + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); + } + +public: + void setup() + { + Wire.begin(); + lightMeter.begin(); + } + + 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 < minReadingInterval) + { + return; + } + + bool shouldUpdate = now - lastSend > maxReadingInterval; + + float lux = lightMeter.readLightLevel(); + lastMeasurement = millis(); + getLuminanceComplete = true; + + if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) + { + lastLux = lux; + lastSend = millis(); + if (WLED_MQTT_CONNECTED) + { + char subuf[45]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/luminance")); + mqtt->publish(subuf, 0, true, String(lux).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_json = 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_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux_json.add(F(" sec until read")); + return; + } + + lux_json.add(lastLux); + lux_json.add(F(" lx")); + } + + uint16_t getId() + { + return USERMOD_ID_BH1750; + } + + /** + * 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(_maxReadInterval)] = maxReadingInterval; + top[FPSTR(_minReadInterval)] = minReadingInterval; + top[FPSTR(_offset)] = offset; + + DEBUG_PRINTLN(F("Photoresistor config saved.")); + } + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + */ + bool readFromConfig(JsonObject &root) + { + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) + { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + disabled = !(top[FPSTR(_enabled)] | !disabled); + maxReadingInterval = (top[FPSTR(_maxReadInterval)] | maxReadingInterval); // ms + minReadingInterval = (top[FPSTR(_minReadInterval)] | minReadingInterval); // ms + offset = top[FPSTR(_offset)] | offset; + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(" config (re)loaded.")); + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; +const char Usermod_BH1750::_enabled[] PROGMEM = "enabled"; +const char Usermod_BH1750::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; +const char Usermod_BH1750::_minReadInterval[] PROGMEM = "min-read-interval-ms"; +const char Usermod_BH1750::_offset[] PROGMEM = "offset-lx"; diff --git a/usermods/BH1750_v2/usermods_list.cpp b/usermods/BH1750_v2/usermods_list.cpp new file mode 100644 index 00000000..6e914df5 --- /dev/null +++ b/usermods/BH1750_v2/usermods_list.cpp @@ -0,0 +1,14 @@ +#include "wled.h" +/* + * Register your v2 usermods here! + */ +#ifdef USERMOD_BH1750 +#include "../usermods/BH1750_v2/usermod_BH1750.h" +#endif + +void registerUsermods() +{ +#ifdef USERMOD_BH1750 + usermods.add(new Usermod_BH1750()); +#endif +} diff --git a/usermods/JSON_IR_remote/readme.md b/usermods/JSON_IR_remote/readme.md index ee18aa94..2cba06ed 100644 --- a/usermods/JSON_IR_remote/readme.md +++ b/usermods/JSON_IR_remote/readme.md @@ -1,33 +1,33 @@ -# JSON IR remote - -## Purpose - -The JSON IR remote allows users to customize IR remote behavior without writing custom code and compiling. -It also enables using any remote that is compatible with your IR receiver. Using the JSON IR remote, you can -map buttons from any remote to any HTTP request API or JSON API command. - -## Usage - -* Upload the IR config file, named _ir.json_ to your board using the [ip address]/edit url. Pick from one of the included files or create your own. -* On the config > LED settings page, set the correct IR pin. -* On the config > Sync Interfaces page, select "JSON Remote" as the Infrared remote. - -## Modification - -* See if there is a json file with the same number of buttons as your remote. Many remotes will have the same internals and emit the same codes but have different labels. -* In the ir.json file, each key will be the hex encoded IR code. -* The "cmd" property will be the HTTP Request API or JSON API to execute when that button is pressed. -* A limited number of c functions are supported (!incBrightness, !decBrightness, !presetFallback) -* When using !presetFallback, include properties PL (preset to load), FX (effect to fall back to) and FP (palette to fall back to) -* If the command is _repeatable_ and does not contain the "~" character, add a "rpt": true property. -* Other properties are ignored, but having a label property may help when editing. - - -Sample: -{ - "0xFF629D": {"cmd": "T=2", "rpt": true, "label": "Toggle on/off"}, // HTTP command - "0xFF9867": {"cmd": "A=~16", "label": "Inc brightness"}, // HTTP command with incrementing - "0xFF38C7": {"cmd": {"bri": 10}, "label": "Dim to 10"}, // JSON command - "0xFF22DD": {"cmd": "!presetFallback", "PL": 1, "FX": 16, "FP": 6, - "label": "Preset 1 or fallback to Saw - Party"}, // c function -} +# JSON IR remote + +## Purpose + +The JSON IR remote allows users to customize IR remote behavior without writing custom code and compiling. +It also enables using any remote that is compatible with your IR receiver. Using the JSON IR remote, you can +map buttons from any remote to any HTTP request API or JSON API command. + +## Usage + +* Upload the IR config file, named _ir.json_ to your board using the [ip address]/edit url. Pick from one of the included files or create your own. +* On the config > LED settings page, set the correct IR pin. +* On the config > Sync Interfaces page, select "JSON Remote" as the Infrared remote. + +## Modification + +* See if there is a json file with the same number of buttons as your remote. Many remotes will have the same internals and emit the same codes but have different labels. +* In the ir.json file, each key will be the hex encoded IR code. +* The "cmd" property will be the HTTP Request API or JSON API to execute when that button is pressed. +* A limited number of c functions are supported (!incBrightness, !decBrightness, !presetFallback) +* When using !presetFallback, include properties PL (preset to load), FX (effect to fall back to) and FP (palette to fall back to) +* If the command is _repeatable_ and does not contain the "~" character, add a "rpt": true property. +* Other properties are ignored, but having a label property may help when editing. + + +Sample: +{ + "0xFF629D": {"cmd": "T=2", "rpt": true, "label": "Toggle on/off"}, // HTTP command + "0xFF9867": {"cmd": "A=~16", "label": "Inc brightness"}, // HTTP command with incrementing + "0xFF38C7": {"cmd": {"bri": 10}, "label": "Dim to 10"}, // JSON command + "0xFF22DD": {"cmd": "!presetFallback", "PL": 1, "FX": 16, "FP": 6, + "label": "Preset 1 or fallback to Saw - Party"}, // c function +} diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 2978b491..6682dde3 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -1,409 +1,409 @@ -#pragma once - -#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. - * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. - * - * - * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * - * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. - * Multiple v2 usermods can be added to one compilation easily. - * - * Creating a usermod: - * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template. - * Please remember to rename the class and file to a descriptive name. - * You may also use multiple .h and .cpp files. - * - * Using a usermod: - * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) - * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp - */ - -class PIRsensorSwitch : public Usermod -{ -public: - /** - * constructor - */ - PIRsensorSwitch() {} - /** - * desctructor - */ - ~PIRsensorSwitch() {} - - /** - * Enable/Disable the PIR sensor - */ - void EnablePIRsensor(bool en) { enabled = en; } - /** - * Get PIR sensor enabled/disabled state - */ - bool PIRsensorEnabled() { return enabled; } - -private: - // PIR sensor pin - int8_t PIRsensorPin = PIR_SENSOR_PIN; - // notification mode for colorUpdated() - const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE - // delay before switch off after the sensor state goes LOW - uint32_t m_switchOffDelay = 600000; // 10min - // off timer start time - uint32_t m_offTimerStart = 0; - // current PIR sensor pin state - 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; - // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR) - bool m_offOnly = false; - bool PIRtriggered = 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[]; - static const char _offOnly[]; - - /** - * check if it is daytime - * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime - */ - 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 (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; - PIRtriggered = switchOn; - 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) { - 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() - { - bool pinState = digitalRead(PIRsensorPin); - if (pinState != sensorPinState) { - sensorPinState = pinState; // change previous state - - if (sensorPinState == HIGH) { - m_offTimerStart = 0; - if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); - publishMqtt("on"); - } else /*if (bri != 0)*/ { - // start switch off timer - m_offTimerStart = millis(); - } - return true; - } - return false; - } - - /** - * switch off the strip if the delay has elapsed - */ - bool handleOffTimer() - { - if (m_offTimerStart > 0 && millis() - m_offTimerStart > m_switchOffDelay) - { - if (enabled == true) - { - if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(false); - publishMqtt("off"); - } - m_offTimerStart = 0; - return true; - } - return false; - } - -public: - //Functions called by WLED - - /** - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - */ - void setup() - { - if (enabled) { - // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (PIRsensorPin >= 0 && pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { - // PIR Sensor mode INPUT_PULLUP - pinMode(PIRsensorPin, INPUT_PULLUP); - sensorPinState = digitalRead(PIRsensorPin); - } else { - if (PIRsensorPin >= 0) { - DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); - } - PIRsensorPin = -1; // allocation failed - enabled = false; - } - } - initDone = true; - } - - /** - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() - { - } - - /** - * loop() is called continuously. Here you can check for events, read sensors, etc. - */ - void loop() - { - // only check sensors 4x/s - if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; - lastLoop = millis(); - - if (!updatePIRsensorState()) { - handleOffTimer(); - } - } - - /** - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * - * Add PIR sensor state and switch off timer duration to jsoninfo - */ - void addToJsonInfo(JsonObject &root) - { - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - if (enabled) - { - // off timer - String uiDomString = F("PIR "); - JsonArray infoArr = user.createNestedArray(uiDomString); // timer value - if (m_offTimerStart > 0) - { - 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")); - } - } 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 - */ -/* - 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; - top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; - top["pin"] = PIRsensorPin; - top[FPSTR(_onPreset)] = m_onPreset; - top[FPSTR(_offPreset)] = m_offPreset; - top[FPSTR(_nightTime)] = m_nightTimeOnly; - top[FPSTR(_mqttOnly)] = m_mqttOnly; - top[FPSTR(_offOnly)] = m_offOnly; - DEBUG_PRINTLN(F("PIR config saved.")); - } - - /** - * restore the changeable values - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - * - * The function should return true if configuration was successfully loaded or false if there was no configuration. - */ - bool readFromConfig(JsonObject &root) - { - bool oldEnabled = enabled; - int8_t oldPin = PIRsensorPin; - - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - PIRsensorPin = top["pin"] | PIRsensorPin; - - enabled = top[FPSTR(_enabled)] | enabled; - - m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; - - m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; - m_onPreset = max(0,min(250,(int)m_onPreset)); - - m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; - m_offPreset = max(0,min(250,(int)m_offPreset)); - - m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; - m_mqttOnly = top[FPSTR(_mqttOnly)] | m_mqttOnly; - m_offOnly = top[FPSTR(_offOnly)] | m_offOnly; - - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // reading config prior to setup() - DEBUG_PRINTLN(F(" config loaded.")); - } else { - if (oldPin != PIRsensorPin || oldEnabled != enabled) { - // check if pin is OK - if (oldPin != PIRsensorPin && oldPin >= 0) { - // if we are changing pin in settings page - // deallocate old pin - pinManager.deallocatePin(oldPin, PinOwner::UM_PIR); - if (pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { - pinMode(PIRsensorPin, INPUT_PULLUP); - } else { - // allocation failed - PIRsensorPin = -1; - enabled = false; - } - } - if (enabled) { - sensorPinState = digitalRead(PIRsensorPin); - } - } - DEBUG_PRINTLN(F(" config (re)loaded.")); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_offOnly)].isNull(); - } - - /** - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_PIRSWITCH; - } -}; - -// strings to reduce flash memory usage (used more than twice) -const char PIRsensorSwitch::_name[] PROGMEM = "PIRsensorSwitch"; -const char PIRsensorSwitch::_enabled[] PROGMEM = "PIRenabled"; -const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec"; -const char PIRsensorSwitch::_onPreset[] PROGMEM = "on-preset"; -const char PIRsensorSwitch::_offPreset[] PROGMEM = "off-preset"; -const char PIRsensorSwitch::_nightTime[] PROGMEM = "nighttime-only"; -const char PIRsensorSwitch::_mqttOnly[] PROGMEM = "mqtt-only"; -const char PIRsensorSwitch::_offOnly[] PROGMEM = "off-only"; +#pragma once + +#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. + * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. + * + * + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. + * Multiple v2 usermods can be added to one compilation easily. + * + * Creating a usermod: + * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template. + * Please remember to rename the class and file to a descriptive name. + * You may also use multiple .h and .cpp files. + * + * Using a usermod: + * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) + * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp + */ + +class PIRsensorSwitch : public Usermod +{ +public: + /** + * constructor + */ + PIRsensorSwitch() {} + /** + * desctructor + */ + ~PIRsensorSwitch() {} + + /** + * Enable/Disable the PIR sensor + */ + void EnablePIRsensor(bool en) { enabled = en; } + /** + * Get PIR sensor enabled/disabled state + */ + bool PIRsensorEnabled() { return enabled; } + +private: + // PIR sensor pin + int8_t PIRsensorPin = PIR_SENSOR_PIN; + // notification mode for colorUpdated() + const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE + // delay before switch off after the sensor state goes LOW + uint32_t m_switchOffDelay = 600000; // 10min + // off timer start time + uint32_t m_offTimerStart = 0; + // current PIR sensor pin state + 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; + // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR) + bool m_offOnly = false; + bool PIRtriggered = 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[]; + static const char _offOnly[]; + + /** + * check if it is daytime + * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime + */ + 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 (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; + PIRtriggered = switchOn; + 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) { + 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() + { + bool pinState = digitalRead(PIRsensorPin); + if (pinState != sensorPinState) { + sensorPinState = pinState; // change previous state + + if (sensorPinState == HIGH) { + m_offTimerStart = 0; + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); + publishMqtt("on"); + } else /*if (bri != 0)*/ { + // start switch off timer + m_offTimerStart = millis(); + } + return true; + } + return false; + } + + /** + * switch off the strip if the delay has elapsed + */ + bool handleOffTimer() + { + if (m_offTimerStart > 0 && millis() - m_offTimerStart > m_switchOffDelay) + { + if (enabled == true) + { + if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(false); + publishMqtt("off"); + } + m_offTimerStart = 0; + return true; + } + return false; + } + +public: + //Functions called by WLED + + /** + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + if (enabled) { + // pin retrieved from cfg.json (readFromConfig()) prior to running setup() + if (PIRsensorPin >= 0 && pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { + // PIR Sensor mode INPUT_PULLUP + pinMode(PIRsensorPin, INPUT_PULLUP); + sensorPinState = digitalRead(PIRsensorPin); + } else { + if (PIRsensorPin >= 0) { + DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); + } + PIRsensorPin = -1; // allocation failed + enabled = false; + } + } + initDone = true; + } + + /** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + } + + /** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() + { + // only check sensors 4x/s + if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; + lastLoop = millis(); + + if (!updatePIRsensorState()) { + handleOffTimer(); + } + } + + /** + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * + * Add PIR sensor state and switch off timer duration to jsoninfo + */ + void addToJsonInfo(JsonObject &root) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + if (enabled) + { + // off timer + String uiDomString = F("PIR "); + JsonArray infoArr = user.createNestedArray(uiDomString); // timer value + if (m_offTimerStart > 0) + { + 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")); + } + } 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 + */ +/* + 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; + top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; + top["pin"] = PIRsensorPin; + top[FPSTR(_onPreset)] = m_onPreset; + top[FPSTR(_offPreset)] = m_offPreset; + top[FPSTR(_nightTime)] = m_nightTimeOnly; + top[FPSTR(_mqttOnly)] = m_mqttOnly; + top[FPSTR(_offOnly)] = m_offOnly; + DEBUG_PRINTLN(F("PIR config saved.")); + } + + /** + * restore the changeable values + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject &root) + { + bool oldEnabled = enabled; + int8_t oldPin = PIRsensorPin; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + PIRsensorPin = top["pin"] | PIRsensorPin; + + enabled = top[FPSTR(_enabled)] | enabled; + + m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; + + m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; + m_onPreset = max(0,min(250,(int)m_onPreset)); + + m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; + m_offPreset = max(0,min(250,(int)m_offPreset)); + + m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; + m_mqttOnly = top[FPSTR(_mqttOnly)] | m_mqttOnly; + m_offOnly = top[FPSTR(_offOnly)] | m_offOnly; + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // reading config prior to setup() + DEBUG_PRINTLN(F(" config loaded.")); + } else { + if (oldPin != PIRsensorPin || oldEnabled != enabled) { + // check if pin is OK + if (oldPin != PIRsensorPin && oldPin >= 0) { + // if we are changing pin in settings page + // deallocate old pin + pinManager.deallocatePin(oldPin, PinOwner::UM_PIR); + if (pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { + pinMode(PIRsensorPin, INPUT_PULLUP); + } else { + // allocation failed + PIRsensorPin = -1; + enabled = false; + } + } + if (enabled) { + sensorPinState = digitalRead(PIRsensorPin); + } + } + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_offOnly)].isNull(); + } + + /** + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_PIRSWITCH; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char PIRsensorSwitch::_name[] PROGMEM = "PIRsensorSwitch"; +const char PIRsensorSwitch::_enabled[] PROGMEM = "PIRenabled"; +const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec"; +const char PIRsensorSwitch::_onPreset[] PROGMEM = "on-preset"; +const char PIRsensorSwitch::_offPreset[] PROGMEM = "off-preset"; +const char PIRsensorSwitch::_nightTime[] PROGMEM = "nighttime-only"; +const char PIRsensorSwitch::_mqttOnly[] PROGMEM = "mqtt-only"; +const char PIRsensorSwitch::_offOnly[] PROGMEM = "off-only"; diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md new file mode 100644 index 00000000..a40098c1 --- /dev/null +++ b/usermods/PWM_fan/readme.md @@ -0,0 +1,36 @@ +# PWM fan + +v2 Usermod to to control PWM fan with RPM feedback and temperature control + +This usermod requires Dallas Temperature usermod to obtain temperature information. If this is not available the fan will always run at 100% speed. +If the fan does not have _tacho_ (RPM) output you can set the _tacho-pin_ to -1 to not use that feature. + +You can also set the thershold temperature at which fan runs at lowest speed. If the actual temperature measured will be 3°C greater than threshold temperature the fan will run at 100%. + +If the _tacho_ is supported the current speed (in RPM) will be repored in WLED Info page. + +## Installation + +Add the compile-time option `-D USERMOD_PWM_FAN` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_PWM_FAN` in `myconfig.h`. +You will also need `-D USERMOD_DALLASTEMPERATURE`. + +### Define Your Options + +All of the parameters are configured during run-time using Usermods settings page. +This includes: + +* PWM output pin +* tacho input pin +* sampling frequency in seconds +* threshold temperature in degees C + +_NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency. + +### PlatformIO requirements + +No special requirements. + +## Change Log + +2021-10 +* First public release diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h new file mode 100644 index 00000000..82aa917b --- /dev/null +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -0,0 +1,332 @@ +#pragma once + +#ifndef USERMOD_DALLASTEMPERATURE +#error The "PWM fan" usermod requires "Dallas Temeprature" usermod to function properly. +#endif + +#include "wled.h" + +// PWM & tacho code curtesy of @KlausMu +// https://github.com/KlausMu/esp32-fan-controller/tree/main/src +// adapted for WLED usermod by @blazoncek + + +// tacho counter +static volatile unsigned long counter_rpm = 0; +// Interrupt counting every rotation of the fan +// https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/ +static void IRAM_ATTR rpm_fan() { + counter_rpm++; +} + + +class PWMFanUsermod : public Usermod { + + private: + + bool initDone = false; + bool enabled = true; + unsigned long msLastTachoMeasurement = 0; + uint16_t last_rpm = 0; + #ifdef ARDUINO_ARCH_ESP32 + uint8_t pwmChannel = 255; + #endif + + #ifdef USERMOD_DALLASTEMPERATURE + UsermodTemperature* tempUM; + #endif + + // configurable parameters + int8_t tachoPin = -1; + int8_t pwmPin = -1; + uint8_t tachoUpdateSec = 30; + float targetTemperature = 25.0; + uint8_t minPWMValuePct = 50; + uint8_t numberOfInterrupsInOneSingleRotation = 2; // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups. + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _tachoPin[]; + static const char _pwmPin[]; + static const char _temperature[]; + static const char _tachoUpdateSec[]; + static const char _minPWMValuePct[]; + static const char _IRQperRotation[]; + + void initTacho(void) { + if (tachoPin < 0 || !pinManager.allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ + tachoPin = -1; + return; + } + pinMode(tachoPin, INPUT); + digitalWrite(tachoPin, HIGH); + attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); + DEBUG_PRINTLN(F("Tacho sucessfully initialized.")); + } + + void deinitTacho(void) { + if (tachoPin < 0) return; + detachInterrupt(digitalPinToInterrupt(tachoPin)); + pinManager.deallocatePin(tachoPin, PinOwner::UM_Unspecified); + tachoPin = -1; + } + + void updateTacho(void) { + if (tachoPin < 0) return; + + // start of tacho measurement + // detach interrupt while calculating rpm + detachInterrupt(digitalPinToInterrupt(tachoPin)); + // calculate rpm + last_rpm = (counter_rpm * 60) / numberOfInterrupsInOneSingleRotation; + last_rpm /= tachoUpdateSec; + // reset counter + counter_rpm = 0; + // store milliseconds when tacho was measured the last time + msLastTachoMeasurement = millis(); + // attach interrupt again + attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); + } + + // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/ + void initPWMfan(void) { + if (pwmPin < 0 || !pinManager.allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { + pwmPin = -1; + return; + } + + #ifdef ESP8266 + analogWriteRange(255); + analogWriteFreq(WLED_PWM_FREQ); + #else + pwmChannel = pinManager.allocateLedc(1); + if (pwmChannel == 255) { //no more free LEDC channels + deinitPWMfan(); return; + } + // configure LED PWM functionalitites + ledcSetup(pwmChannel, 25000, 8); + // attach the channel to the GPIO to be controlled + ledcAttachPin(pwmPin, pwmChannel); + #endif + DEBUG_PRINTLN(F("Fan PWM sucessfully initialized.")); + } + + void deinitPWMfan(void) { + if (pwmPin < 0) return; + + pinManager.deallocatePin(pwmPin, PinOwner::UM_Unspecified); + #ifdef ARDUINO_ARCH_ESP32 + pinManager.deallocateLedc(pwmChannel, 1); + #endif + pwmPin = -1; + } + + void updateFanSpeed(uint8_t pwmValue){ + if (pwmPin < 0) return; + + #ifdef ESP8266 + analogWrite(pwmPin, pwmValue); + #else + ledcWrite(pwmChannel, pwmValue); + #endif + } + + float getActualTemperature(void) { + #ifdef USERMOD_DALLASTEMPERATURE + if (tempUM != nullptr) + return tempUM->getTemperatureC(); + #endif + return -127.0f; + } + + void setFanPWMbasedOnTemperature(void) { + float temp = getActualTemperature(); + float difftemp = temp - targetTemperature; + // Default to run fan at full speed. + int newPWMvalue = 255; + int pwmStep = ((100 - minPWMValuePct) * newPWMvalue) / (7*100); + int pwmMinimumValue = (minPWMValuePct * newPWMvalue) / 100; + + if ((temp == NAN) || (temp <= 0.0)) { + DEBUG_PRINTLN(F("WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255.")); + } else if (difftemp <= 0.0) { + // Temperature is below target temperature. Run fan at minimum speed. + newPWMvalue = pwmMinimumValue; + } else if (difftemp <= 0.5) { + newPWMvalue = pwmMinimumValue + pwmStep; + } else if (difftemp <= 1.0) { + newPWMvalue = pwmMinimumValue + 2*pwmStep; + } else if (difftemp <= 1.5) { + newPWMvalue = pwmMinimumValue + 3*pwmStep; + } else if (difftemp <= 2.0) { + newPWMvalue = pwmMinimumValue + 4*pwmStep; + } else if (difftemp <= 2.5) { + newPWMvalue = pwmMinimumValue + 5*pwmStep; + } else if (difftemp <= 3.0) { + newPWMvalue = pwmMinimumValue + 6*pwmStep; + } + updateFanSpeed(newPWMvalue); + } + + public: + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { + #ifdef USERMOD_DALLASTEMPERATURE + // This Usermod requires Temperature usermod + tempUM = (UsermodTemperature*) usermods.lookup(USERMOD_ID_TEMPERATURE); + #endif + initTacho(); + initPWMfan(); + updateFanSpeed((minPWMValuePct * 255) / 100); // inital fan speed + initDone = true; + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /* + * Da loop. + */ + void loop() { + if (!enabled || strip.isUpdating()) return; + + unsigned long now = millis(); + if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return; + + updateTacho(); + setFanPWMbasedOnTemperature(); + } + + /* + * 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) { + if (tachoPin < 0) return; + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray data = user.createNestedArray(FPSTR(_name)); + data.add(last_rpm); + data.add(F("rpm")); + } + + /* + * 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) { + // 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. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_pwmPin)] = pwmPin; + top[FPSTR(_tachoPin)] = tachoPin; + top[FPSTR(_tachoUpdateSec)] = tachoUpdateSec; + top[FPSTR(_temperature)] = targetTemperature; + top[FPSTR(_minPWMValuePct)] = minPWMValuePct; + top[FPSTR(_IRQperRotation)] = numberOfInterrupsInOneSingleRotation; + DEBUG_PRINTLN(F("Autosave config saved.")); + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject& root) { + int8_t newTachoPin = tachoPin; + int8_t newPwmPin = pwmPin; + + JsonObject top = root[FPSTR(_name)]; + DEBUG_PRINT(FPSTR(_name)); + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + newTachoPin = top[FPSTR(_tachoPin)] | newTachoPin; + newPwmPin = top[FPSTR(_pwmPin)] | newPwmPin; + tachoUpdateSec = top[FPSTR(_tachoUpdateSec)] | tachoUpdateSec; + tachoUpdateSec = (uint8_t) max(1,(int)tachoUpdateSec); // bounds checking + targetTemperature = top[FPSTR(_temperature)] | targetTemperature; + minPWMValuePct = top[FPSTR(_minPWMValuePct)] | minPWMValuePct; + minPWMValuePct = (uint8_t) min(100,max(0,(int)minPWMValuePct)); // bounds checking + numberOfInterrupsInOneSingleRotation = top[FPSTR(_IRQperRotation)] | numberOfInterrupsInOneSingleRotation; + numberOfInterrupsInOneSingleRotation = (uint8_t) max(1,(int)numberOfInterrupsInOneSingleRotation); // bounds checking + + if (!initDone) { + // first run: reading from cfg.json + tachoPin = newTachoPin; + pwmPin = newPwmPin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing paramters from settings page + if (tachoPin != newTachoPin || pwmPin != newPwmPin) { + DEBUG_PRINTLN(F("Re-init pins.")); + // deallocate pin and release interrupts + deinitTacho(); + deinitPWMfan(); + tachoPin = newTachoPin; + pwmPin = newPwmPin; + // initialise + setup(); + } + } + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_IRQperRotation)].isNull(); + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_PWM_FAN; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char PWMFanUsermod::_name[] PROGMEM = "PWM-fan"; +const char PWMFanUsermod::_enabled[] PROGMEM = "enabled"; +const char PWMFanUsermod::_tachoPin[] PROGMEM = "tacho-pin"; +const char PWMFanUsermod::_pwmPin[] PROGMEM = "PWM-pin"; +const char PWMFanUsermod::_temperature[] PROGMEM = "target-temp-C"; +const char PWMFanUsermod::_tachoUpdateSec[] PROGMEM = "tacho-update-s"; +const char PWMFanUsermod::_minPWMValuePct[] PROGMEM = "min-PWM-percent"; +const char PWMFanUsermod::_IRQperRotation[] PROGMEM = "IRQs-per-rotation"; diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index c4229047..7c209f47 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -81,7 +81,9 @@ class UsermodTemperature : public Usermod { temperature = readDallas(); lastMeasurement = millis(); waitingForConversion = false; - DEBUG_PRINTF("Read temperature %2.1f.\n", temperature); + //DEBUG_PRINTF("Read temperature %2.1f.\n", temperature); // does not work properly on 8266 + DEBUG_PRINT(F("Read temperature ")); + DEBUG_PRINTLN(temperature); } bool findSensor() { diff --git a/usermods/battery_status_basic/assets/battery_connection_schematic_01.png b/usermods/battery_status_basic/assets/battery_connection_schematic_01.png new file mode 100644 index 00000000..5ce01de6 Binary files /dev/null and b/usermods/battery_status_basic/assets/battery_connection_schematic_01.png differ diff --git a/usermods/battery_status_basic/assets/battery_connection_schematic_02.png b/usermods/battery_status_basic/assets/battery_connection_schematic_02.png new file mode 100644 index 00000000..03f41ca0 Binary files /dev/null and b/usermods/battery_status_basic/assets/battery_connection_schematic_02.png differ diff --git a/usermods/battery_status_basic/assets/battery_info_screen.png b/usermods/battery_status_basic/assets/battery_info_screen.png new file mode 100644 index 00000000..50eb5346 Binary files /dev/null and b/usermods/battery_status_basic/assets/battery_info_screen.png differ diff --git a/usermods/battery_status_basic/readme.md b/usermods/battery_status_basic/readme.md index 7bff98f4..276b23c1 100644 --- a/usermods/battery_status_basic/readme.md +++ b/usermods/battery_status_basic/readme.md @@ -2,16 +2,25 @@ This Usermod allows you to monitor the battery level of your battery powered project. -You can see the battery level in the `info modal` right under the `estimated current`. +You can see the battery level and voltage in the `info modal`. For this to work the positive side of the (18650) battery must be connected to pin `A0` of the d1mini/esp8266 with a 100k ohm resistor (see [Useful Links](#useful-links)). If you have a esp32 board it is best to connect the positive side of the battery to ADC1 (GPIO32 - GPIO39) +

+ +

+ ## Installation define `USERMOD_BATTERY_STATUS_BASIC` in `my_config.h` +### Basic wiring diagram +

+ +

+ ### Define Your Options * `USERMOD_BATTERY_STATUS_BASIC` - define this (in `my_config.h`) to have this user mod included wled00\usermods_list.cpp @@ -45,6 +54,11 @@ Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3. * https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/ ## Change Log +2021-09-02 +* added "Battery voltage" to info +* added circuit diagram to readme +* added MQTT support, sending battery voltage +* minor fixes 2021-08-15 * changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries diff --git a/usermods/battery_status_basic/usermod_v2_battery_status_basic.h b/usermods/battery_status_basic/usermod_v2_battery_status_basic.h index f6271c27..ab9cba3b 100644 --- a/usermods/battery_status_basic/usermod_v2_battery_status_basic.h +++ b/usermods/battery_status_basic/usermod_v2_battery_status_basic.h @@ -29,7 +29,7 @@ #endif -// the frequency to check the battery, 1 minute +// the frequency to check the battery, 30 sec #ifndef USERMOD_BATTERY_MEASUREMENT_INTERVAL #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000 #endif @@ -53,7 +53,8 @@ class UsermodBatteryBasic : public Usermod int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; - unsigned long lastTime = 0; + unsigned long nextReadTime = 0; + unsigned long lastReadTime = 0; // battery min. voltage float minBatteryVoltage = USERMOD_BATTERY_MIN_VOLTAGE; // battery max. voltage @@ -68,6 +69,7 @@ class UsermodBatteryBasic : public Usermod // mapped battery level based on voltage long batteryLevel = 0; bool initDone = false; + bool initializing = true; // strings to reduce flash memory usage (used more than twice) @@ -82,6 +84,19 @@ class UsermodBatteryBasic : public Usermod return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } + float truncate(float val, byte dec) + { + float x = val * pow(10, dec); + float y = round(x); + float z = x - y; + if ((int)z == 5) + { + y++; + } + x = y / pow(10, dec); + return x; + } + public: @@ -107,6 +122,9 @@ class UsermodBatteryBasic : public Usermod pinMode(batteryPin, INPUT); #endif + nextReadTime = millis() + readingInterval; + lastReadTime = millis(); + initDone = true; } @@ -129,26 +147,38 @@ class UsermodBatteryBasic : public Usermod { if(strip.isUpdating()) return; - unsigned long now = millis(); - // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms) - if (now - lastTime >= readingInterval) { + if (millis() < nextReadTime) return; - // read battery raw input - rawValue = analogRead(batteryPin); - // calculate the voltage - voltage = (rawValue / adcPrecision) * maxBatteryVoltage ; + nextReadTime = millis() + readingInterval; + lastReadTime = millis(); + initializing = false; - // translate battery voltage into percentage - /* - the standard "map" function doesn't work - https://www.arduino.cc/reference/en/language/functions/math/map/ notes and warnings at the bottom - */ - batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); + // read battery raw input + rawValue = analogRead(batteryPin); - lastTime = now; + // calculate the voltage + voltage = (rawValue / adcPrecision) * maxBatteryVoltage ; + // check if voltage is within specified voltage range + voltage = voltagemaxBatteryVoltage?-1.0f:voltage; + + // translate battery voltage into percentage + /* + the standard "map" function doesn't work + https://www.arduino.cc/reference/en/language/functions/math/map/ notes and warnings at the bottom + */ + batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); + + + // SmartHome stuff + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/voltage")); + mqtt->publish(subuf, 0, false, String(voltage).c_str()); } + } @@ -163,9 +193,31 @@ class UsermodBatteryBasic : public Usermod JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonArray battery = user.createNestedArray("Battery level"); - battery.add(batteryLevel); - battery.add(F(" %")); + // info modal display names + JsonArray batteryPercentage = user.createNestedArray("Battery level"); + JsonArray batteryVoltage = user.createNestedArray("Battery voltage"); + + if (initializing) { + batteryPercentage.add((nextReadTime - millis()) / 1000); + batteryPercentage.add(" sec"); + batteryVoltage.add((nextReadTime - millis()) / 1000); + batteryVoltage.add(" sec"); + return; + } + + if(batteryLevel < 0) { + batteryPercentage.add(F("invalid")); + } else { + batteryPercentage.add(batteryLevel); + } + batteryPercentage.add(F(" %")); + + if(voltage < 0) { + batteryVoltage.add(F("invalid")); + } else { + batteryVoltage.add(truncate(voltage, 2)); + } + batteryVoltage.add(F(" V")); } diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index cd3e01c7..c3d55d66 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -317,15 +317,15 @@ class MultiRelay : public Usermod { * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - void addToJsonState(JsonObject &root) { - } + //void addToJsonState(JsonObject &root) { + //} /** * 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) { + //} /** * provide the changeable values @@ -335,11 +335,12 @@ class MultiRelay : public Usermod { top[FPSTR(_enabled)] = enabled; for (uint8_t i=0; isetBusClock(ioFrequency); // can be used for SPI too 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 @@ -683,6 +686,7 @@ class FourLineDisplayUsermod : public Usermod { top[FPSTR(_screenTimeOut)] = screenTimeout/1000; top[FPSTR(_sleepMode)] = (bool) sleepMode; top[FPSTR(_clockMode)] = (bool) clockMode; + top[FPSTR(_busClkFrequency)] = ioFrequency/1000; DEBUG_PRINTLN(F("4 Line Display config saved.")); } @@ -714,6 +718,7 @@ class FourLineDisplayUsermod : public Usermod { screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; sleepMode = top[FPSTR(_sleepMode)] | sleepMode; clockMode = top[FPSTR(_clockMode)] | clockMode; + ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency DEBUG_PRINT(FPSTR(_name)); if (!initDone) { @@ -739,12 +744,13 @@ class FourLineDisplayUsermod : public Usermod { setup(); needsRedraw |= true; } + if (!(type == SSD1306_SPI || type == SSD1306_SPI64)) u8x8->setBusClock(ioFrequency); // can be used for SPI too setContrast(contrast); setFlipMode(flip); if (needsRedraw && !wakeDisplay()) redraw(true); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !(top["pin"][2]).isNull(); + return !(top[_busClkFrequency]).isNull(); } /* @@ -757,10 +763,11 @@ class FourLineDisplayUsermod : public Usermod { }; // 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"; +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"; +const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md new file mode 100644 index 00000000..67cde353 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -0,0 +1,45 @@ +# I2C 4 Line Display Usermod ALT + +Thank you to the authors of the original version of these usermods. It would not have been possible without them! +"usermod_v2_four_line_display" +"usermod_v2_rotary_encoder_ui" + +The core of these usermods are a copy of the originals. The main changes are done to the FourLineDisplay usermod. +The display usermod UI has been completely changed. + + +The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. +Without the display it functions identical to the original. +The original "usermod_v2_auto_save" will not work with the display just yet. + +Press the encoder to cycle through the options: + *Brightness + *Speed + *Intensity + *Palette + *Effect + *Main Color (only if display is used) + *Saturation (only if display is used) + +Press and hold the encoder to display Network Info + if AP is active then it will display AP ssid and Password + +Also shows if the timer is enabled + +[See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI) + +## Installation + +Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions +Then to activate this alternative usermod add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, + or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file + + +### PlatformIO requirements + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-10 +* First public release \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h new file mode 100644 index 00000000..3dcb5af6 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -0,0 +1,970 @@ +#pragma once + +#include "wled.h" +#include // from https://github.com/olikraus/u8g2/ + +// +// Insired by the usermod_v2_four_line_display +// +// v2 usermod for using 128x32 or 128x64 i2c +// OLED displays to provide a four line display +// for WLED. +// +// Dependencies +// * This usermod REQURES the ModeSortUsermod +// * This Usermod works best, by far, when coupled +// with RotaryEncoderUIUsermod. +// +// Make sure to enable NTP and set your time zone in WLED Config | Time. +// +// REQUIREMENT: You must add the following requirements to +// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini +// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) +// REQUIREMENT: * Wire +// + +//The SCL and SDA pins are defined here. +#ifdef ARDUINO_ARCH_ESP32 + #ifndef FLD_PIN_SCL + #define FLD_PIN_SCL 22 + #endif + #ifndef FLD_PIN_SDA + #define FLD_PIN_SDA 21 + #endif + #ifndef FLD_PIN_CLOCKSPI + #define FLD_PIN_CLOCKSPI 18 + #endif + #ifndef FLD_PIN_DATASPI + #define FLD_PIN_DATASPI 23 + #endif + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 19 + #endif + #ifndef FLD_PIN_CS + #define FLD_PIN_CS 5 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 26 + #endif +#else + #ifndef FLD_PIN_SCL + #define FLD_PIN_SCL 5 + #endif + #ifndef FLD_PIN_SDA + #define FLD_PIN_SDA 4 + #endif + #ifndef FLD_PIN_CLOCKSPI + #define FLD_PIN_CLOCKSPI 14 + #endif + #ifndef FLD_PIN_DATASPI + #define FLD_PIN_DATASPI 13 + #endif + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 12 + #endif + #ifndef FLD_PIN_CS + #define FLD_PIN_CS 15 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 16 + #endif +#endif + +// When to time out to the clock or blank the screen +// if SLEEP_MODE_ENABLED. +#define SCREEN_TIMEOUT_MS 60*1000 // 1 min + +#define TIME_INDENT 0 +#define DATE_INDENT 2 + +// Minimum time between redrawing screen in ms +#define USER_LOOP_REFRESH_RATE_MS 100 + +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 16+1 +#define MAX_JSON_CHARS 19+1 +#define MAX_MODE_LINE_SPACE 13+1 + +typedef enum { + NONE = 0, + SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C + SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C + SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C + SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C + SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C + SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI + SSD1306_SPI64 // U8X8_SSD1306_128X64_NONAME_HW_SPI +} DisplayType; + +/* + Fontname: benji_custom_icons_1x + Copyright: + Glyphs: 1/1 + BBX Build Mode: 3 + * 4 = custom palette +*/ +const uint8_t u8x8_font_benji_custom_icons_1x1[13] U8X8_FONT_SECTION("u8x8_font_benji_custom_icons_1x1") = + "\4\4\1\1<\370\360\3\17\77yy\377\377\377\377\317\17\17" + "\17\17\7\3\360\360\360\360\366\377\377\366\360\360\360\360\0\0\0\0\377\377\377\377\237\17\17\237\377\377\377\377" + "\6\17\17\6\340\370\374\376\377\340\200\0\0\0\0\0\0\0\0\0\3\17\37\77\177\177\177\377\376|||" + "\70\30\14\0\0\0\0\0\0\0\0``\360\370|<\36\7\2\0\300\360\376\377\177\77\36\0\1\1\0" + "\0\0\0\0\200\200\14\14\300\340\360\363\363\360\340\300\14\14\200\200\1\1\60\60\3\4\10\310\310\10\4\3" + "\60\60\1\1"; + +/* + Fontname: benji_custom_icons_6x + Copyright: + Glyphs: 8/8 + BBX Build Mode: 3 + // 6x6 icons libraries take up a lot of memory thus all the icons uses are consolidated into a single library + // these are just the required icons stripped from the U8x8 libraries in addition to a few new custom icons + * 1 = sun + * 2 = skip forward + * 3 = fire + * 4 = custom palette + * 5 = puzzle piece + * 6 = moon + * 7 = brush + * 8 = custom saturation +*/ +const uint8_t u8x8_font_benji_custom_icons_6x6[2308] U8X8_FONT_SECTION("u8x8_font_benji_custom_icons_6x6") = + "\1\10\6\6\0\0\0\0\0\0\200\300\300\300\300\200\0\0\0\0\0\0\0\0\0\36\77\77\77\77\36\0" + "\0\0\0\0\0\0\0\0\200\300\300\300\300\200\0\0\0\0\0\0\0\0\0\0\0\0\7\17\17\17\17\7" + "\0\0\0\0\200\300\340\340\340\360\360\360\360\360\360\340\340\340\300\200\0\0\0\0\7\17\17\17\17\7\0\0" + "\0\0\0\0\300\340\340\340\340\300\0\0\0\0\0\0\340\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\376\374\340\0\0\0\0\0\0\300\340\340\340\340\300\3\7\7\7\7\3\0\0\0\0\0\0" + "\7\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\7\0\0\0\0\0\0\3\7" + "\7\7\7\3\0\0\0\0\0\0\340\360\360\360\360\340\0\0\0\0\1\3\7\7\7\17\17\17\17\17\17\7" + "\7\7\3\1\0\0\0\0\340\360\360\360\360\340\0\0\0\0\0\0\0\0\0\0\0\0\1\3\3\3\3\1" + "\0\0\0\0\0\0\0\0\0x\374\374\374\374x\0\0\0\0\0\0\0\0\0\1\3\3\3\3\1\0\0" + "\0\0\0\0\300\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\200\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\376\376\374\370\360\360\340\300\200" + "\200\0\0\0\0\0\0\0\0\0\0\0\377\377\377\376\376\374\370\360\360\340\300\200\200\0\0\0\0\0\0\0" + "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\374\374\370\360\340\340\300\200\0\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\376\374\374\370\360\340\340\300\200\0\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\177\77\77\37\17\7\7\3\1\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\77\37\17\7" + "\7\3\1\0\377\377\377\177\177\77\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\0\377\377\377\177" + "\177\77\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\0\3\1\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\376\374\374\370\360\340\300\200\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\340\360\374" + "\377\377\377\377\377\377\377\377\377\376\370\300\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\300\340\360\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\37\0\0\0\0" + "\0\0\4\370\360\360\340\300\200\0\0\0\0\0\0\0\0\0\0\0\370\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\177\77\37\7\3\0\0\0\0\0\200\300\360\374\377\377\377\377\377\377\377\376\370\340\0\0\0" + "\0\0\0\0\3\37\177\377\377\377\377\377\377\377\377\377\77\17\7\1\0\0\0\0\0\200\300\360\370\374\376\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\1\3\7\17\37\77\77\177\200" + "\0\0\0\0\0\0\340\374\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\17\1\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\200\300\340\340\360\360\370|<>>>~\377\377\377\377\377\377\377\177" + "\77\36\36\36\36<|\370\370\360\360\340\340\200\0\0\0\0\0\0\0\0\300\360\374\376\377\377\377\377\377\377" + "\377\360\340\300\300\300\300\340\360\377\377\377\377\377\377\370\360\340\340\340\340\360\370\377\377\377\377\377\377\377\377\377" + "\374\360\340\200\360\377\377\377\377\377\207\3\1\1\1\1\3\207\377\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\207\3\1\1\1\1\3\207\377\377\377\377\377\17\377\377\377\377\377\377\377\376~>>" + "\77\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\376\376\376\377\377\377" + "\177\77\37\7\0\0\3\17\77\177\377\377\360\340\300\300\300\300\340\360\377\377\377\377\377\377\377\377\377\377\77\17" + "\17\7\7\7\7\7\7\7\7\7\3\3\3\3\1\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37" + "\37\77\77\177\177\177\377\377\377\377\377\377\377\377\377~\30\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\370\374\376\377\377\377\377\377\377\376\374\360\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\360\360\360\360\360\360\360\360\360\360\360\360" + "\360\363\377\377\377\377\377\377\377\377\363\360\360\360\360\360\360\360\360\360\360\360\360\360\0\0\0\0\0\0\0\0" + "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\374\374\376\376\377\377\377\377" + "\377\376\374\360\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\17\17\17\17\17\17\37\77\177\377\377\377\377" + "\377\377\377\377\377\377\377\377\3\3\7\7\17\17\17\17\7\7\3\0\377\377\377\377\377\377\377\377\377\377\377\377" + "\360\300\0\0\0\0\0\0\0\0\300\360\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\200\300\340\360\360\370\374\374\376\376\7\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\360\374\376\377\377\377\377\377\377\377" + "\377\377\377\340\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\374\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\374\360\300\200\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\17\177\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\376\374\370\360\360\340\340\300\300\300\200\200\200\200\0\0\0\0\0\0\200\200" + "\200\200\0\0\0\0\1\7\37\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" + "\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\7\1\0\0\0\0\0\0\0\0\0\0\1\3\3\7" + "\17\17\37\37\37\77\77\77\77\177\177\177\177\177\177\77\77\77\77\37\37\37\17\17\7\3\3\1\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\200\200\300\340\360\360\370\374\374\376\377~\34\10\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\200\300\300\340\360\360\370\374\376\376\377\377\377\377\377\377\177\77\17\7\3" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\4\6\17\17\37\77\177\377" + "\377\377\377\377\377\377\77\37\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\370\374\376" + "\376\377\377\377\377\377\377\376\376\374\370\340\0\0\0\0\3\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\200\360\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\17\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0`px\374\376\377\377\377\377\377\377" + "\177\177\177\77\77\37\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\200\300\300\200\0\0\0\0\0\0\0\0\0\14\36\77\77\36\14\0\0" + "\0\0\0\0\0\0\0\200\300\300\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\3\7\17\17\7\3" + "\0\200\300\340\360\360\370\370\370\374\374\374\374\370\370\370\360\360\340\300\200\0\3\7\17\17\7\3\0\0\0\0" + "\0\0\0\0\300\340\360\360\340\300\0\0\0\0\340\374\377\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177" + "\177\177\177\177\177\377\374\340\0\0\0\0\300\340\360\360\340\300\0\0\0\1\3\3\1\0\0\0\0\0\1\17" + "\77\177\370\340\300\200\200\0\0\0\0\0\0\0\0\200\200\300\340\370\177\77\17\1\0\0\0\0\0\1\3\3" + "\1\0\0\0\0\0\0\0\0\0\60x\374\374x\60\0\0\0\1\3\3\7\7\7\16\16\16\16\7\7\7" + "\3\3\1\0\0\0\60x\374\374x\60\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\14\36\77\77\36\14\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0"; + +class FourLineDisplayUsermod : public Usermod { + + private: + + bool initDone = false; + unsigned long lastTime = 0; + + // HW interface & configuration + U8X8 *u8x8 = nullptr; // pointer to U8X8 display object + #ifndef FLD_SPI_DEFAULT + int8_t ioPin[5] = {FLD_PIN_SCL, FLD_PIN_SDA, -1, -1, -1}; // I2C pins: SCL, SDA + uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) + DisplayType type = SSD1306_64; // display type + #else + int8_t ioPin[5] = {FLD_PIN_CLOCKSPI, FLD_PIN_DATASPI, FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // SPI pins: CLK, MOSI, CS, DC, RST + DisplayType type = SSD1306_SPI; // display type + #endif + 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; + + // Next variables hold the previous known values to determine if redraw is + // required. + String knownSsid = ""; + IPAddress knownIp; + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + uint8_t knownMinute = 99; + byte brightness100; + byte fxspeed100; + byte fxintensity100; + bool knownnightlight = nightlightActive; + bool wificonnected = interfacesInited; + bool powerON = true; + + bool displayTurnedOff = false; + unsigned long lastUpdate = 0; + unsigned long lastRedraw = 0; + unsigned long overlayUntil = 0; + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 0; + byte markColNum = 0; + + // 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[]; + static const char _busClkFrequency[]; + + // 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() { + if (type == NONE) return; + if (type == SSD1306_SPI || type == SSD1306_SPI64) { + PinManagerPinType pins[5] = { { ioPin[0], true }, { ioPin[1], true}, { ioPin[2], true }, { ioPin[3], true}, { ioPin[4], true }}; + if (!pinManager.allocateMultiplePins(pins, 5, PinOwner::UM_FourLineDisplay)) { type=NONE; return; } + } else { + PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true} }; + if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_FourLineDisplay)) { type=NONE; return; } + } + DEBUG_PRINTLN(F("Allocating display.")); + switch (type) { + case SSD1306: + #ifdef ESP8266 + if (!(ioPin[0]==5 && ioPin[1]==4)) + u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + lineHeight = 1; + break; + case SH1106: + #ifdef ESP8266 + if (!(ioPin[0]==5 && ioPin[1]==4)) + u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + lineHeight = 2; + break; + case SSD1306_64: + #ifdef ESP8266 + if (!(ioPin[0]==5 && ioPin[1]==4)) + u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + lineHeight = 2; + break; + case SSD1305: + #ifdef ESP8266 + if (!(ioPin[0]==5 && ioPin[1]==4)) + u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + lineHeight = 1; + break; + case SSD1305_64: + #ifdef ESP8266 + if (!(ioPin[0]==5 && ioPin[1]==4)) + u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + lineHeight = 2; + break; + case SSD1306_SPI: + if (!(ioPin[0]==FLD_PIN_CLOCKSPI && ioPin[1]==FLD_PIN_DATASPI)) // if not overridden these sould be HW accellerated + u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); + else + u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset + lineHeight = 1; + break; + case SSD1306_SPI64: + if (!(ioPin[0]==FLD_PIN_CLOCKSPI && ioPin[1]==FLD_PIN_DATASPI)) // if not overridden these sould be HW accellerated + u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); + else + u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset + lineHeight = 2; + break; + default: + u8x8 = nullptr; + } + if (nullptr == u8x8) { + DEBUG_PRINTLN(F("Display init failed.")); + for (byte i=0; i<5 && ioPin[i]>=0; i++) pinManager.deallocatePin(ioPin[i], PinOwner::UM_FourLineDisplay); + type = NONE; + return; + } + + initDone = true; + DEBUG_PRINTLN(F("Starting display.")); + if (!(type == SSD1306_SPI || type == SSD1306_SPI64)) u8x8->setBusClock(ioFrequency); // can be used for SPI too + 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..."); + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /** + * Da loop. + */ + void loop() { + if (displayTurnedOff && millis() - lastUpdate < 1000) { + return; + }else if (millis() - lastUpdate < refreshRate){ + return;} + redraw(false); + lastUpdate = millis(); + } + + /** + * Wrappers for screen drawing + */ + void setFlipMode(uint8_t mode) { + if (type==NONE) return; + u8x8->setFlipMode(mode); + } + void setContrast(uint8_t contrast) { + if (type==NONE) return; + u8x8->setContrast(contrast); + } + void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { + if (type==NONE) return; + u8x8->setFont(u8x8_font_chroma48medium8_r); + if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); + else u8x8->drawString(col, row, string); + } + void draw2x2String(uint8_t col, uint8_t row, const char *string) { + if (type==NONE) return; + u8x8->setFont(u8x8_font_chroma48medium8_r); + 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; + u8x8->setFont(font); + if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); + else u8x8->drawGlyph(col, row, glyph); + } + uint8_t getCols() { + if (type==NONE) return 0; + return u8x8->getCols(); + } + void clear() { + if (type==NONE) return; + u8x8->clear(); + } + void setPowerSave(uint8_t save) { + if (type==NONE) return; + u8x8->setPowerSave(save); + } + + void center(String &line, uint8_t width) { + int len = line.length(); + if (len0; i--) line = ' ' + line; + for (byte i=line.length(); i 0) { + if (millis() >= overlayUntil) { + // Time to display the overlay has elapsed. + overlayUntil = 0; + forceRedraw = true; + } else { + // We are still displaying the overlay + // Don't redraw. + return; + } + } + + + // Check if values which are shown on display changed from the last time. + if (forceRedraw) { + needRedraw = true; + } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon + powerON = !powerON; + drawStatusIcons(); + lastRedraw = millis(); + } else if (knownnightlight != nightlightActive) { //trigger moon icon + knownnightlight = nightlightActive; + drawStatusIcons(); + if (knownnightlight) overlay(" Timer On", 1000, 6); + lastRedraw = millis(); + }else if (wificonnected != interfacesInited){ //trigger wifi icon + wificonnected = interfacesInited; + drawStatusIcons(); + lastRedraw = millis(); + } else if (knownMode != effectCurrent) { + knownMode = effectCurrent; + if(displayTurnedOff)needRedraw = true; + else showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); + } else if (knownPalette != effectPalette) { + knownPalette = effectPalette; + if(displayTurnedOff)needRedraw = true; + else showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); + } else if (knownBrightness != bri) { + if(displayTurnedOff && nightlightActive){needRedraw = false; knownBrightness = bri;} + else if(displayTurnedOff)needRedraw = true; + else updateBrightness(); + } else if (knownEffectSpeed != effectSpeed) { + if(displayTurnedOff)needRedraw = true; + else updateSpeed(); + } else if (knownEffectIntensity != effectIntensity) { + if(displayTurnedOff)needRedraw = true; + else updateIntensity(); + } + + + if (!needRedraw) { + // Nothing to change. + // Turn off display after 1 minutes with no change. + if(sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { + // We will still check if there is a change in redraw() + // and turn it back on if it changed. + sleepOrClock(true); + } else if (displayTurnedOff && clockMode) { + showTime(); + } + return; + } else { + clear(); + } + + needRedraw = false; + lastRedraw = millis(); + + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + } + + // Update last known values. + knownSsid = apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : + knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); + knownBrightness = bri; + knownMode = effectCurrent; + knownPalette = effectPalette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownnightlight = nightlightActive; + wificonnected = interfacesInited; + + // Do the actual drawing + // First row: Icons + draw2x2GlyphIcons(); + drawArrow(); + drawStatusIcons(); + + // Second row + updateBrightness(); + updateSpeed(); + updateIntensity(); + + // Third row + showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info + + // Fourth row + showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info + } + + void updateBrightness(){ + knownBrightness = bri; + if(overlayUntil == 0){ + brightness100 = (((float)(bri)/255)*100); + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), brightness100); + drawString(1, lineHeight, lineBuffer); + lastRedraw = millis();} + } + + void updateSpeed(){ + knownEffectSpeed = effectSpeed; + if(overlayUntil == 0){ + fxspeed100 = (((float)(effectSpeed)/255)*100); + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), fxspeed100); + drawString(5, lineHeight, lineBuffer); + lastRedraw = millis();} + } + + void updateIntensity(){ + knownEffectIntensity = effectIntensity; + if(overlayUntil == 0){ + fxintensity100 = (((float)(effectIntensity)/255)*100); + char lineBuffer[4]; + sprintf_P(lineBuffer, PSTR("%-3d"), fxintensity100); + drawString(9, lineHeight, lineBuffer); + lastRedraw = millis();} + } + + void draw2x2GlyphIcons(){ + if(lineHeight == 2){ + drawGlyph(1, 0, 1, u8x8_font_benji_custom_icons_2x2, true);//brightness icon + drawGlyph(5, 0, 2, u8x8_font_benji_custom_icons_2x2, true);//speed icon + drawGlyph(9, 0, 3, u8x8_font_benji_custom_icons_2x2, true);//intensity icon + drawGlyph(14, 2*lineHeight, 4, u8x8_font_benji_custom_icons_2x2, true);//palette icon + drawGlyph(14, 3*lineHeight, 5, u8x8_font_benji_custom_icons_2x2, true);//effect icon + } + else{ + drawGlyph(2, 0, 69, u8x8_font_open_iconic_weather_1x1);//brightness icon + drawGlyph(6, 0, 72, u8x8_font_open_iconic_play_1x1);//speed icon + drawGlyph(10, 0, 78, u8x8_font_open_iconic_thing_1x1);//intensity icon + drawGlyph(15, 2*lineHeight, 4, u8x8_font_benji_custom_icons_1x1);//palette icon + drawGlyph(15, 3*lineHeight, 70, u8x8_font_open_iconic_thing_1x1);//effect icon + } + } + + void drawStatusIcons(){ + drawGlyph(14, 0, 80 + (wificonnected?0:1), u8x8_font_open_iconic_embedded_1x1, true); // wifi icon + drawGlyph(15, 0, 78 + (bri > 0 ? 0 : 3), u8x8_font_open_iconic_embedded_1x1, true); // power icon + drawGlyph(13, 0, 66 + (nightlightActive?0:4), u8x8_font_open_iconic_weather_1x1, true); // moon icon for nighlight mode + } + + /** + * marks the position of the arrow showing + * the current setting being changed + * pass line and colum info + */ + void setMarkLine(byte newMarkLineNum, byte newMarkColNum) { + markLineNum = newMarkLineNum; + markColNum = newMarkColNum; + } + + //Draw the arrow for the current setting beiong changed + void drawArrow(){ + if(markColNum != 255 && markLineNum !=255)drawGlyph(markColNum, markLineNum*lineHeight, 69, u8x8_font_open_iconic_play_1x1); + } + + //Display the current effect or palette (desiredEntry) + // on the appropriate line (row). + void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { + knownMode = effectCurrent; + knownPalette = effectPalette; + if(overlayUntil == 0){ + char lineBuffer[MAX_JSON_CHARS]; + char smallBuffer1[MAX_MODE_LINE_SPACE]; + char smallBuffer2[MAX_MODE_LINE_SPACE]; + char smallBuffer3[MAX_MODE_LINE_SPACE+1]; + uint8_t qComma = 0; + bool insideQuotes = false; + bool spaceHit = false; + uint8_t printedChars = 0; + uint8_t smallChars1 = 0; + uint8_t smallChars2 = 0; + uint8_t smallChars3 = 0; + uint8_t totalCount = 0; + char singleJsonSymbol; + + // Find the mode name in JSON + for (size_t i = 0; i < strlen_P(qstring); i++) { //find and get the full text for printing + singleJsonSymbol = pgm_read_byte_near(qstring + i); + if (singleJsonSymbol == '\0') break; + switch (singleJsonSymbol) { + case '"': + insideQuotes = !insideQuotes; + break; + case '[': + case ']': + break; + case ',': + qComma++; + default: + if (!insideQuotes || (qComma != inputEffPal)) break; + lineBuffer[printedChars++] = singleJsonSymbol; + totalCount++; + } + if ((qComma > inputEffPal)) break; + } + + if(lineHeight ==2){ // use this code for 8 line display + if(printedChars < (MAX_MODE_LINE_SPACE)){ // use big font if the text fits + for (;printedChars < (MAX_MODE_LINE_SPACE-1); printedChars++) {lineBuffer[printedChars]=' '; } + lineBuffer[printedChars] = 0; + drawString(1, row*lineHeight, lineBuffer); + lastRedraw = millis(); + }else{ // for long names divide the text into 2 lines and print them small + for (uint8_t i = 0; i < printedChars; i++){ + switch (lineBuffer[i]){ + case ' ': + if(i > 4 && !spaceHit) { + spaceHit = true; + break;} + if(!spaceHit) smallBuffer1[smallChars1++] = lineBuffer[i]; + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + break; + default: + if(!spaceHit) smallBuffer1[smallChars1++] = lineBuffer[i]; + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + break; + } + } + for (; smallChars1 < (MAX_MODE_LINE_SPACE-1); smallChars1++) smallBuffer1[smallChars1]=' '; + smallBuffer1[smallChars1] = 0; + drawString(1, row*lineHeight, smallBuffer1, true); + for (; smallChars2 < (MAX_MODE_LINE_SPACE-1); smallChars2++) smallBuffer2[smallChars2]=' '; + smallBuffer2[smallChars2] = 0; + drawString(1, row*lineHeight+1, smallBuffer2, true); + lastRedraw = millis(); + } + } + else{ // use this code for 4 ling displays + if (printedChars > MAX_MODE_LINE_SPACE) printedChars = MAX_MODE_LINE_SPACE; + for (uint8_t i = 0; i < printedChars; i++){ + smallBuffer3[smallChars3++] = lineBuffer[i]; + } + + for (; smallChars3 < (MAX_MODE_LINE_SPACE); smallChars3++) smallBuffer3[smallChars3]=' '; + smallBuffer3[smallChars3] = 0; + drawString(1, row*lineHeight, smallBuffer3, true); + lastRedraw = millis(); + } + } + } + + /** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ + bool wakeDisplay() { + //knownHour = 99; + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + redraw(true); + return true; + } + return false; + } + + /** + * Allows you to show one line and a glyph as overlay for a + * period of time. + * Clears the screen and prints. + */ + void overlay(const char* line1, long showHowLong, byte glyphType) { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + } + + // Print the overlay + clear(); + if (glyphType > 0){ + if ( lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_font_benji_custom_icons_6x6, true); + else drawGlyph(7, lineHeight, glyphType, u8x8_font_benji_custom_icons_2x2, true); + } + if (line1) drawString(0, 3*lineHeight, line1); + overlayUntil = millis() + showHowLong; + } + + void networkOverlay(const char* line1, long showHowLong) { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + } + // Print the overlay + clear(); + // First row string + if (line1) drawString(0, 0, line1); + // Second row with Wifi name + String ssidString = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); // + drawString(0, lineHeight, ssidString.c_str()); + // Print `~` char to indicate that SSID is longer, than our display + if (knownSsid.length() > getCols()) { + drawString(getCols() - 1, 0, "~"); + } + // Third row with IP and Psssword in AP Mode + drawString(0, lineHeight*2, (knownIp.toString()).c_str()); + if (apActive) { + String appassword = apPass; + drawString(0, lineHeight*3, appassword.c_str()); + } + overlayUntil = millis() + showHowLong; + } + + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled) { + if (enabled) { + if (clockMode) { + clear(); + knownMinute = 99; + showTime(); + }else setPowerSave(1); + displayTurnedOff = true; + } + else { + setPowerSave(0); + displayTurnedOff = false; + } + } + + /** + * Display the current date and time in large characters + * on the middle rows. Based 24 or 12 hour depending on + * the useAMPM configuration. + */ + void showTime() { + if(knownMinute != minute(localTime)){ //only redraw clock if it has changed + char lineBuffer[LINE_BUFFER_SIZE]; + + //updateLocalTime(); + byte AmPmHour = hour(localTime); + boolean isitAM = true; + if (useAMPM) { + if (AmPmHour > 11) AmPmHour -= 12; + if (AmPmHour == 0) AmPmHour = 12; + if (hour(localTime) > 11) isitAM = false; + } + clear(); + drawStatusIcons(); //icons power, wifi, timer, etc + + sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); + draw2x2String(DATE_INDENT, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day + + sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hour(localTime)), minute(localTime)); + draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds + + if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time + knownMinute = minute(localTime); + } + } + + /* + * 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) { + //} + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void readFromJsonState(JsonObject& root) { + // if (!initDone) return; // prevent crash on boot applyPreset() + //} + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + JsonArray io_pin = top.createNestedArray("pin"); + for (byte i=0; i<5; i++) io_pin.add(ioPin[i]); + top["help4PinTypes"] = F("Clk,Data,CS,DC,RST"); // help for Settings page + top["type"] = type; + top[FPSTR(_flip)] = (bool) flip; + top[FPSTR(_contrast)] = contrast; + top[FPSTR(_refreshRate)] = refreshRate/10; + top[FPSTR(_screenTimeOut)] = screenTimeout/1000; + top[FPSTR(_sleepMode)] = (bool) sleepMode; + top[FPSTR(_clockMode)] = (bool) clockMode; + top[FPSTR(_busClkFrequency)] = ioFrequency/1000; + DEBUG_PRINTLN(F("4 Line Display config saved.")); + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ + bool readFromConfig(JsonObject& root) { + bool needsRedraw = false; + DisplayType newType = type; + int8_t newPin[5]; for (byte i=0; i<5; i++) newPin[i] = ioPin[i]; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + newType = top["type"] | newType; + for (byte i=0; i<5; i++) newPin[i] = top["pin"][i] | ioPin[i]; + flip = top[FPSTR(_flip)] | flip; + contrast = top[FPSTR(_contrast)] | contrast; + refreshRate = (top[FPSTR(_refreshRate)] | refreshRate/10) * 10; + screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; + sleepMode = top[FPSTR(_sleepMode)] | sleepMode; + clockMode = top[FPSTR(_clockMode)] | clockMode; + ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + for (byte i=0; i<5; i++) ioPin[i] = newPin[i]; + type = newType; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + bool pinsChanged = false; + for (byte i=0; i<5; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } + if (pinsChanged || type!=newType) { + if (type != NONE) delete u8x8; + for (byte i=0; i<5; i++) { + if (ioPin[i]>=0) pinManager.deallocatePin(ioPin[i], PinOwner::UM_FourLineDisplay); + ioPin[i] = newPin[i]; + } + if (ioPin[0]<0 || ioPin[1]<0) { // data & clock must be > -1 + type = NONE; + return true; + } else type = newType; + setup(); + needsRedraw |= true; + } + if (!(type == SSD1306_SPI || type == SSD1306_SPI64)) u8x8->setBusClock(ioFrequency); // can be used for SPI too + setContrast(contrast); + setFlipMode(flip); + if (needsRedraw && !wakeDisplay()) redraw(true); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !(top[_busClkFrequency]).isNull(); + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_FOUR_LINE_DISP; + } +}; + +// 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 = "refreshRate0.01Sec"; +const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec"; +const char FourLineDisplayUsermod::_flip[] PROGMEM = "flip"; +const char FourLineDisplayUsermod::_sleepMode[] PROGMEM = "sleepMode"; +const char FourLineDisplayUsermod::_clockMode[] PROGMEM = "clockMode"; +const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md new file mode 100644 index 00000000..a140f25b --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -0,0 +1,45 @@ +# Rotary Encoder UI Usermod ALT + +Thank you to the authors of the original version of these usermods. It would not have been possible without them! +"usermod_v2_four_line_display" +"usermod_v2_rotary_encoder_ui" + +The core of these usermods are a copy of the originals. The main changes are done to the FourLineDisplay usermod. +The display usermod UI has been completely changed. + + +The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. +Without the display it functions identical to the original. +The original "usermod_v2_auto_save" will not work with the display just yet. + +Press the encoder to cycle through the options: + *Brightness + *Speed + *Intensity + *Palette + *Effect + *Main Color (only if display is used) + *Saturation (only if display is used) + +Press and hold the encoder to display Network Info + if AP is active then it will display AP ssid and Password + +Also shows if the timer is enabled + +[See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI) + +## Installation + +Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions +Then to activate this alternative usermod add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, + or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file + + +### PlatformIO requirements + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-10 +* First public release \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h new file mode 100644 index 00000000..625af0af --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -0,0 +1,569 @@ +#pragma once + +#include "wled.h" + +// +// Inspired by the original v2 usermods +// * usermod_v2_rotaty_encoder_ui +// +// v2 usermod that provides a rotary encoder-based UI. +// +// This usermod allows you to control: +// +// * Brightness +// * Selected Effect +// * Effect Speed +// * Effect Intensity +// * Palette +// +// Change between modes by pressing a button. +// +// Dependencies +// * This usermod REQURES the ModeSortUsermod +// * This Usermod works best coupled with +// FourLineDisplayUsermod. +// +// If FourLineDisplayUsermod is used the folowing options are also inabled +// +// * main color +// * saturation of main color +// * display network (long press buttion) +// + +#ifndef ENCODER_DT_PIN +#define ENCODER_DT_PIN 18 +#endif + +#ifndef ENCODER_CLK_PIN +#define ENCODER_CLK_PIN 5 +#endif + +#ifndef ENCODER_SW_PIN +#define ENCODER_SW_PIN 19 +#endif + +// The last UI state, remove color and saturation option if diplay not active(too many options) +#ifdef USERMOD_FOUR_LINE_DISPLAY + #define LAST_UI_STATE 6 +#else + #define LAST_UI_STATE 4 +#endif + + +class RotaryEncoderUIUsermod : public Usermod { +private: + int fadeAmount = 5; // Amount to change every step (brightness) + unsigned long currentTime; + unsigned long loopTime; + unsigned long buttonHoldTIme; + int8_t pinA = ENCODER_DT_PIN; // DT from encoder + int8_t pinB = ENCODER_CLK_PIN; // CLK from encoder + int8_t pinC = ENCODER_SW_PIN; // SW from encoder + unsigned char select_state = 0; // 0: brightness, 1: effect, 2: effect speed + unsigned char button_state = HIGH; + unsigned char prev_button_state = HIGH; + bool networkShown = false; + uint16_t currentHue1 = 6425; // default reboot color + byte currentSat1 = 255; + +#ifdef USERMOD_FOUR_LINE_DISPLAY + FourLineDisplayUsermod *display; +#else + void* display = nullptr; +#endif + + byte *modes_alpha_indexes = nullptr; + byte *palettes_alpha_indexes = nullptr; + + unsigned char Enc_A; + unsigned char Enc_B; + unsigned char Enc_A_prev = 0; + + bool currentEffectAndPaletteInitialized = false; + uint8_t effectCurrentIndex = 0; + uint8_t effectPaletteIndex = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + + bool initDone = false; + bool enabled = true; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _DT_pin[]; + static const char _CLK_pin[]; + static const char _SW_pin[]; + +public: + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; + if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { + // BUG: configuring this usermod with conflicting pins + // will cause it to de-allocate pins it does not own + // (at second config) + // This is the exact type of bug solved by pinManager + // tracking the owner tags.... + pinA = pinB = pinC = -1; + enabled = false; + return; + } + + pinMode(pinA, INPUT_PULLUP); + pinMode(pinB, INPUT_PULLUP); + pinMode(pinC, INPUT_PULLUP); + currentTime = millis(); + loopTime = currentTime; + + ModeSortUsermod *modeSortUsermod = (ModeSortUsermod*) usermods.lookup(USERMOD_ID_MODE_SORT); + modes_alpha_indexes = modeSortUsermod->getModesAlphaIndexes(); + palettes_alpha_indexes = modeSortUsermod->getPalettesAlphaIndexes(); + +#ifdef USERMOD_FOUR_LINE_DISPLAY + // This Usermod uses FourLineDisplayUsermod for the best experience. + // But it's optional. But you want it. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + if (display != nullptr) { + display->setMarkLine(1, 0); + } +#endif + + initDone = true; + Enc_A = digitalRead(pinA); // Read encoder pins + Enc_B = digitalRead(pinB); + Enc_A_prev = Enc_A; + } + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + //Serial.println("Connected to WiFi!"); + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ + void loop() + { + currentTime = millis(); // get the current elapsed time + + // Initialize effectCurrentIndex and effectPaletteIndex to + // current state. We do it here as (at least) effectCurrent + // is not yet initialized when setup is called. + + if (!currentEffectAndPaletteInitialized) { + findCurrentEffectAndPalette();} + + if(modes_alpha_indexes[effectCurrentIndex] != effectCurrent + || palettes_alpha_indexes[effectPaletteIndex] != effectPalette){ + currentEffectAndPaletteInitialized = false; + } + + if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz + { + button_state = digitalRead(pinC); + if (prev_button_state != button_state) + { + if (button_state == HIGH && (millis()-buttonHoldTIme < 3000)) + { + prev_button_state = button_state; + + char newState = select_state + 1; + if (newState > LAST_UI_STATE) newState = 0; + + bool changedState = true; + if (display != nullptr) { + switch(newState) { + case 0: + changedState = changeState(" Brightness", 1, 0, 1); + break; + case 1: + changedState = changeState(" Speed", 1, 4, 2); + break; + case 2: + changedState = changeState(" Intensity", 1 ,8, 3); + break; + case 3: + changedState = changeState(" Color Palette", 2, 0, 4); + break; + case 4: + changedState = changeState(" Effect", 3, 0, 5); + break; + case 5: + changedState = changeState(" Main Color", 255, 255, 7); + break; + case 6: + changedState = changeState(" Saturation", 255, 255, 8); + break; + } + } + if (changedState) { + select_state = newState; + } + } + else + { + prev_button_state = button_state; + networkShown = false; + if(!prev_button_state)buttonHoldTIme = millis(); + } + } + + if (!prev_button_state && (millis()-buttonHoldTIme > 3000) && !networkShown) displayNetworkInfo(); //long press for network info + + Enc_A = digitalRead(pinA); // Read encoder pins + Enc_B = digitalRead(pinB); + if ((Enc_A) && (!Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse + { // B is high so clockwise + switch(select_state) { + case 0: + changeBrightness(true); + break; + case 1: + changeEffectSpeed(true); + break; + case 2: + changeEffectIntensity(true); + break; + case 3: + changePalette(true); + break; + case 4: + changeEffect(true); + break; + case 5: + changeHue(true); + break; + case 6: + changeSat(true); + break; + } + } + else if (Enc_B == HIGH) + { // B is low so counter-clockwise + switch(select_state) { + case 0: + changeBrightness(false); + break; + case 1: + changeEffectSpeed(false); + break; + case 2: + changeEffectIntensity(false); + break; + case 3: + changePalette(false); + break; + case 4: + changeEffect(false); + break; + case 5: + changeHue(false); + break; + case 6: + changeSat(false); + break; + } + } + } + Enc_A_prev = Enc_A; // Store value of A for next time + loopTime = currentTime; // Updates loopTime + } + } + + void displayNetworkInfo(){ + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->networkOverlay(" NETWORK INFO", 15000); + networkShown = true; + #endif + } + + void findCurrentEffectAndPalette() { + currentEffectAndPaletteInitialized = true; + for (uint8_t i = 0; i < strip.getModeCount(); i++) { + if (modes_alpha_indexes[i] == effectCurrent) { + effectCurrentIndex = i; + break; + } + } + + for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { + if (palettes_alpha_indexes[i] == effectPalette) { + effectPaletteIndex = i; + break; + } + } + } + + boolean changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + if (display->wakeDisplay()) { + // Throw away wake up input + return false; + } + display->overlay(stateName, 750, glyph); + display->setMarkLine(markedLine, markedCol); + } + #endif + return true; + } + + void lampUdated() { + //bool fxChanged = strip.setEffectConfig(effectCurrent, effectSpeed, effectIntensity, effectPalette); + //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(CALL_MODE_DIRECT_CHANGE); + updateInterfaces(CALL_MODE_DIRECT_CHANGE); + } + + void changeBrightness(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } + #endif + if (increase) bri = (bri + fadeAmount <= 255) ? (bri + fadeAmount) : 255; + else bri = (bri - fadeAmount >= 0) ? (bri - fadeAmount) : 0; + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateBrightness(); + #endif + } + + + void changeEffect(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } + #endif + if (increase) effectCurrentIndex = (effectCurrentIndex + 1 >= strip.getModeCount()) ? 0 : (effectCurrentIndex + 1); + else effectCurrentIndex = (effectCurrentIndex - 1 < 0) ? (strip.getModeCount() - 1) : (effectCurrentIndex - 1); + effectCurrent = modes_alpha_indexes[effectCurrentIndex]; + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); + #endif + } + + + void changeEffectSpeed(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } + #endif + if (increase) effectSpeed = (effectSpeed + fadeAmount <= 255) ? (effectSpeed + fadeAmount) : 255; + else effectSpeed = (effectSpeed - fadeAmount >= 0) ? (effectSpeed - fadeAmount) : 0; + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateSpeed(); + #endif + } + + + void changeEffectIntensity(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } + #endif + if (increase) effectIntensity = (effectIntensity + fadeAmount <= 255) ? (effectIntensity + fadeAmount) : 255; + else effectIntensity = (effectIntensity - fadeAmount >= 0) ? (effectIntensity - fadeAmount) : 0; + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateIntensity(); + #endif + } + + + void changePalette(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } + #endif + if (increase) effectPaletteIndex = (effectPaletteIndex + 1 >= strip.getPaletteCount()) ? 0 : (effectPaletteIndex + 1); + else effectPaletteIndex = (effectPaletteIndex - 1 < 0) ? (strip.getPaletteCount() - 1) : (effectPaletteIndex - 1); + effectPalette = palettes_alpha_indexes[effectPaletteIndex]; + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); + #endif + } + + + void changeHue(bool increase){ + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } + #endif + + if(increase) currentHue1 += 321; + else currentHue1 -= 321; + colorHStoRGB(currentHue1, currentSat1, col); + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateRedrawTime(); + #endif + } + + void changeSat(bool increase){ + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } + #endif + + if(increase) currentSat1 = (currentSat1 + 5 <= 255 ? (currentSat1 + 5) : 255); + else currentSat1 = (currentSat1 - 5 >= 0 ? (currentSat1 - 5) : 0); + colorHStoRGB(currentHue1, currentSat1, col); + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + display->updateRedrawTime(); + #endif + + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + /* + void addToJsonInfo(JsonObject& root) + { + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit + } + */ + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject &root) + { + //root["user0"] = userVar0; + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject &root) + { + //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); + } + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root) { + // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_DT_pin)] = pinA; + top[FPSTR(_CLK_pin)] = pinB; + top[FPSTR(_SW_pin)] = pinC; + DEBUG_PRINTLN(F("Rotary Encoder config saved.")); + } + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject &root) { + // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + int8_t newDTpin = pinA; + int8_t newCLKpin = pinB; + int8_t newSWpin = pinC; + + enabled = top[FPSTR(_enabled)] | enabled; + newDTpin = top[FPSTR(_DT_pin)] | newDTpin; + newCLKpin = top[FPSTR(_CLK_pin)] | newCLKpin; + newSWpin = top[FPSTR(_SW_pin)] | newSWpin; + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + pinA = newDTpin; + pinB = newCLKpin; + pinC = newSWpin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin) { + pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); + pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); + pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); + pinA = newDTpin; + pinB = newCLKpin; + pinC = newSWpin; + if (pinA<0 || pinB<0 || pinC<0) { + enabled = false; + return true; + } + setup(); + } + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_enabled)].isNull(); + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_ROTARY_ENC_UI; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; +const char RotaryEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; +const char RotaryEncoderUIUsermod::_DT_pin[] PROGMEM = "DT-pin"; +const char RotaryEncoderUIUsermod::_CLK_pin[] PROGMEM = "CLK-pin"; +const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 55d929e0..380959c1 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -10,6 +10,20 @@ #include "bus_wrapper.h" #include +// enable additional debug output +#ifdef WLED_DEBUG + #ifndef ESP8266 + #include + #endif + #define DEBUG_PRINT(x) Serial.print(x) + #define DEBUG_PRINTLN(x) Serial.println(x) + #define DEBUG_PRINTF(x...) Serial.printf(x) +#else + #define DEBUG_PRINT(x) + #define DEBUG_PRINTLN(x) + #define DEBUG_PRINTF(x...) +#endif + //temporary struct for passing bus configuration to bus struct BusConfig { uint8_t type = TYPE_WS2812_RGB; @@ -23,7 +37,8 @@ struct BusConfig { type = busType; count = len; start = pstart; colorOrder = pcolorOrder; reversed = rev; skipAmount = skip; uint8_t nPins = 1; - if (type > 47) nPins = 2; + if (type >= TYPE_NET_DDP_RGB && type < 96) nPins = 4; //virtual network bus. 4 "pins" store IP address + else if (type > 47) nPins = 2; else if (type > 40 && type < 46) nPins = NUM_PWM_PINS(type); for (uint8_t i = 0; i < nPins; i++) pins[i] = ppins[i]; } @@ -135,7 +150,7 @@ class BusDigital : public Bus { _busPtr = PolyBus::create(_iType, _pins, _len, nr); _valid = (_busPtr != nullptr); _colorOrder = bc.colorOrder; - //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); + DEBUG_PRINTF("Successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u)\n",nr, _len, bc.type, _pins[0],_pins[1],_iType); }; inline void show() { @@ -201,7 +216,7 @@ class BusDigital : public Bus { } void cleanup() { - //Serial.println("Digital Cleanup"); + DEBUG_PRINTLN("Digital Cleanup"); PolyBus::cleanup(_busPtr, _iType); _iType = I_NONE; _valid = false; @@ -227,6 +242,7 @@ class BusDigital : public Bus { class BusPwm : public Bus { public: BusPwm(BusConfig &bc) : Bus(bc.type, bc.start) { + _valid = false; if (!IS_PWM(bc.type)) return; uint8_t numPins = NUM_PWM_PINS(bc.type); @@ -280,10 +296,12 @@ class BusPwm : public Bus { //does no index check uint32_t getPixelColor(uint16_t pix) { + if (!_valid) return 0; return ((_data[3] << 24) | (_data[0] << 16) | (_data[1] << 8) | (_data[2])); } void show() { + if (!_valid) return; uint8_t numPins = NUM_PWM_PINS(_type); for (uint8_t i = 0; i < numPins; i++) { uint8_t scaled = (_data[i] * _bri) / 255; @@ -301,6 +319,7 @@ class BusPwm : public Bus { } uint8_t getPins(uint8_t* pinArray) { + if (!_valid) return 0; uint8_t numPins = NUM_PWM_PINS(_type); for (uint8_t i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; @@ -328,13 +347,13 @@ class BusPwm : public Bus { void deallocatePins() { uint8_t numPins = NUM_PWM_PINS(_type); for (uint8_t i = 0; i < numPins; i++) { + pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); if (!pinManager.isPinOk(_pins[i])) continue; #ifdef ESP8266 digitalWrite(_pins[i], LOW); //turn off PWM interrupt #else if (_ledcStart < 16) ledcDetachPin(_pins[i]); #endif - pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); } #ifdef ARDUINO_ARCH_ESP32 pinManager.deallocateLedc(_ledcStart, numPins); @@ -342,6 +361,116 @@ class BusPwm : public Bus { } }; + +class BusNetwork : public Bus { + public: + BusNetwork(BusConfig &bc) : Bus(bc.type, bc.start) { + _valid = false; +// switch (bc.type) { +// case TYPE_NET_ARTNET_RGB: +// _rgbw = false; +// _UDPtype = 2; +// break; +// case TYPE_NET_E131_RGB: +// _rgbw = false; +// _UDPtype = 1; +// break; +// case TYPE_NET_DDP_RGB: +// _rgbw = false; +// _UDPtype = 0; +// break; +// default: + _rgbw = false; + _UDPtype = bc.type - TYPE_NET_DDP_RGB; +// break; +// } + _UDPchannels = _rgbw ? 4 : 3; + //_rgbw |= bc.rgbwOverride; // RGBW override in bit 7 or can have a special type + _data = (byte *)malloc(bc.count * _UDPchannels); + if (_data == nullptr) return; + memset(_data, 0, bc.count * _UDPchannels); + _len = bc.count; + //_colorOrder = bc.colorOrder; + _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); + _broadcastLock = false; + _valid = true; + }; + + void setPixelColor(uint16_t pix, uint32_t c) { + if (!_valid || pix >= _len) return; + uint16_t offset = pix * _UDPchannels; + _data[offset] = 0xFF & (c >> 16); + _data[offset+1] = 0xFF & (c >> 8); + _data[offset+2] = 0xFF & (c ); + if (_rgbw) _data[offset+3] = 0xFF & (c >> 24); + } + + uint32_t getPixelColor(uint16_t pix) { + if (!_valid || pix >= _len) return 0; + uint16_t offset = pix * _UDPchannels; + return ( + (_rgbw ? (_data[offset+3] << 24) : 0) + | (_data[offset] << 16) + | (_data[offset+1] << 8) + | (_data[offset+2] ) + ); + } + + void show() { + if (!_valid || !canShow()) return; + _broadcastLock = true; + realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw); + _broadcastLock = false; + } + + inline bool canShow() { + // this should be a return value from UDP routine if it is still sending data out + return !_broadcastLock; + } + + inline void setBrightness(uint8_t b) { + _bri = b; + } + + uint8_t getPins(uint8_t* pinArray) { + for (uint8_t i = 0; i < 4; i++) { + pinArray[i] = _client[i]; + } + return 4; + } + + inline bool isRgbw() { + return _rgbw; + } + + inline uint16_t getLength() { + return _len; + } + + void cleanup() { + _type = I_NONE; + _valid = false; + if (_data != nullptr) free(_data); + _data = nullptr; + } + + ~BusNetwork() { + cleanup(); + } + + private: + IPAddress _client; + uint16_t _len = 0; + //uint8_t _colorOrder; + uint8_t _bri = 255; + uint8_t _UDPtype; + uint8_t _UDPchannels; + bool _rgbw; + bool _broadcastLock; + byte *_data; +}; + + class BusManager { public: BusManager() { @@ -365,15 +494,16 @@ class BusManager { return len*6; #endif } - - if (type > 31 && type < 48) return 5; + if (type > 31 && type < 48) return 5; if (type == 44 || type == 45) return len*4; //RGBW return len*3; } int add(BusConfig &bc) { if (numBusses >= WLED_MAX_BUSSES) return -1; - if (IS_DIGITAL(bc.type)) { + if (bc.type >= TYPE_NET_DDP_RGB && bc.type < 96) { + busses[numBusses] = new BusNetwork(bc); + } else if (IS_DIGITAL(bc.type)) { busses[numBusses] = new BusDigital(bc, numBusses); } else { busses[numBusses] = new BusPwm(bc); @@ -402,7 +532,6 @@ class BusManager { uint16_t bstart = b->getStart(); if (pix < bstart || pix >= bstart + b->getLength()) continue; busses[i]->setPixelColor(pix - bstart, c); - break; } } @@ -444,6 +573,7 @@ class BusManager { return len; } + // a workaround static inline bool isRgbw(uint8_t type) { return Bus::isRgbw(type); } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 8adaac2f..1a499404 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -263,6 +263,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject if_live = interfaces["live"]; CJSON(receiveDirect, if_live["en"]); CJSON(e131Port, if_live["port"]); // 5568 + if (e131Port == DDP_DEFAULT_PORT) e131Port = E131_DEFAULT_PORT; // prevent double DDP port allocation CJSON(e131Multicast, if_live[F("mc")]); JsonObject if_live_dmx = if_live[F("dmx")]; diff --git a/wled00/const.h b/wled00/const.h index f7983e64..95faba51 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -59,7 +59,9 @@ #define USERMOD_ID_ELEKSTUBE_IPS 16 //Usermod "usermod_elekstube_ips.h" #define USERMOD_ID_SN_PHOTORESISTOR 17 //Usermod "usermod_sn_photoresistor.h" #define USERMOD_ID_BATTERY_STATUS_BASIC 18 //Usermod "usermod_v2_battery_status_basic.h" -#define USERMOD_ID_SEVEN_SEGMENT_DISPLAY 19 //Usermod "usermod_v2_seven_segment_display.h" +#define USERMOD_ID_PWM_FAN 19 //Usermod "usermod_PWM_fan.h" +#define USERMOD_ID_BH1750 20 //Usermod "usermod_bh1750.h" +#define USERMOD_ID_SEVEN_SEGMENT_DISPLAY 21 //Usermod "usermod_v2_seven_segment_display.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -113,13 +115,17 @@ #define DMX_MODE_MULTIPLE_DRGB 5 //every LED is addressed with its own RGB and share a master dimmer (ledCount * 3 + 1 channels) #define DMX_MODE_MULTIPLE_RGBW 6 //every LED is addressed with its own RGBW (ledCount * 4 channels) -//Light capability byte (unused) 0bRRCCTTTT +//Light capability byte (unused) 0bRCCCTTTT //bits 0/1/2/3: specifies a type of LED driver. A single "driver" may have different chip models but must have the same protocol/behavior -//bits 4/5: specifies the class of LED driver - 0b00 (dec. 0-15) unconfigured/reserved -// - 0b01 (dec. 16-31) digital (data pin only) -// - 0b10 (dec. 32-47) analog (PWM) -// - 0b11 (dec. 48-63) digital (data + clock / SPI) -//bits 6/7 are reserved and set to 0b00 +//bits 4/5/6: specifies the class of LED driver - 0b000 (dec. 0-15) unconfigured/reserved +// - 0b001 (dec. 16-31) digital (data pin only) +// - 0b010 (dec. 32-47) analog (PWM) +// - 0b011 (dec. 48-63) digital (data + clock / SPI) +// - 0b100 (dec. 64-79) unused/reserved +// - 0b101 (dec. 80-95) digital (data + clock / SPI) +// - 0b110 (dec. 96-111) unused/reserved +// - 0b111 (dec. 112-127) unused/reserved +//bit 7 is reserved and set to 0 #define TYPE_NONE 0 //light is not configured #define TYPE_RESERVED 1 //unused. Might indicate a "virtual" light @@ -143,6 +149,10 @@ #define TYPE_APA102 51 #define TYPE_LPD8806 52 #define TYPE_P9813 53 +//Network types (master broadcast) (80-95) +#define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus) +#define TYPE_NET_E131_RGB 81 //network E131 RGB bus (master broadcast bus) +#define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus) #define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63 #define IS_PWM(t) ((t) > 40 && (t) < 46) @@ -242,7 +252,7 @@ #ifndef MAX_LED_MEMORY #ifdef ESP8266 -#define MAX_LED_MEMORY 5000 +#define MAX_LED_MEMORY 4000 #else #define MAX_LED_MEMORY 64000 #endif @@ -283,7 +293,7 @@ // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 - #define WLED_MAX_NODES 15 + #define WLED_MAX_NODES 24 #else #define WLED_MAX_NODES 150 #endif diff --git a/wled00/data/index.js b/wled00/data/index.js index 4f19e003..e77a3797 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -506,12 +506,12 @@ function populatePresets(fromls) pJson["0"] = {}; localStorage.setItem("wledP", JSON.stringify(pJson)); } - pmtLS = pmt; - for (var a = 0; a < is.length; a++) { - let i = is[a]; - if (expanded[i+100]) expand(i+100, true); - } - makePlSel(arr); + pmtLS = pmt; + for (var a = 0; a < is.length; a++) { + let i = is[a]; + if (expanded[i+100]) expand(i+100, true); + } + //makePlSel(arr); } else { presetError(true); } updatePA(); populateQL(); @@ -1296,14 +1296,16 @@ var plJson = {"0":{ "end": 0 }}; -var plSelContent = ""; -function makePlSel(arr) { - plSelContent = ""; +//var plSelContent = ""; +function makePlSel(incPl=false) { + var plSelContent = ""; + var arr = Object.entries(pJson); for (var i = 0; i < arr.length; i++) { var n = arr[i][1].n ? arr[i][1].n : "Preset " + arr[i][0]; - if (arr[i][1].playlist && arr[i][1].playlist.ps) continue; //remove playlists, sub-playlists not yet supported + if (!incPl && arr[i][1].playlist && arr[i][1].playlist.ps) continue; //remove playlists, sub-playlists not yet supported plSelContent += `` } + return plSelContent; } function refreshPlE(p) { @@ -1391,7 +1393,7 @@ function makeP(i,pl) { End preset:
`; @@ -1449,7 +1451,7 @@ function makePlEntry(p,i) { return `
Duration
Transition
#${i+1}

diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 469c72e1..53f1b43d 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -5,10 +5,11 @@ LED Settings )====="; +const char PAGE_settingsCss[] PROGMEM = R"=====()====="; // Autogenerated from wled00/data/settings.htm, do not edit!! @@ -77,18 +77,18 @@ onclick="B()">Back // Autogenerated from wled00/data/settings_leds.htm, do not edit!! const char PAGE_settings_leds[] PROGMEM = R"=====(LED Settings