diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini new file mode 100644 index 000000000..1771fd17a --- /dev/null +++ b/usermods/DHT/platformio_override.ini @@ -0,0 +1,22 @@ +; Options +; ------- +; USERMOD_DHT - define this to have this user mod included wled00\usermods_list.cpp +; USERMOD_DHT_DHTTYPE - DHT model: 11, 21, 22 for DHT11, DHT21, or DHT22, defaults to 22/DHT22 +; USERMOD_DHT_PIN - pin to which DTH is connected, defaults to Q2 pin on QuinLed Dig-Uno's board +; USERMOD_DHT_CELSIUS - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported +; USERMOD_DHT_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds +; USERMOD_DHT_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 90 seconds +; USERMOD_DHT_STATS - For debug, report delay stats + +[env:d1_mini_usermod_dht_C] +extends = env:d1_mini +build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT -D USERMOD_DHT_CELSIUS +lib_deps = ${env.lib_deps} + https://github.com/alwynallan/DHT_nonblocking + +[env:custom32_LEDPIN_16_usermod_dht_C] +extends = env:custom32_LEDPIN_16 +build_flags = ${env:custom32_LEDPIN_16.build_flags} -D USERMOD_DHT -D USERMOD_DHT_CELSIUS -D USERMOD_DHT_STATS +lib_deps = ${env.lib_deps} + https://github.com/alwynallan/DHT_nonblocking + diff --git a/usermods/DHT/readme.md b/usermods/DHT/readme.md new file mode 100644 index 000000000..5a123d4bd --- /dev/null +++ b/usermods/DHT/readme.md @@ -0,0 +1,41 @@ +# DHT Temperature/Humidity sensor usermod + +This usermod will read from an attached DHT22 or DHT11 humidity and temperature sensor. +The sensor readings are displayed in the Info section of the web UI. + +If sensor is not detected after a while (10 update intervals), this usermod will be disabled. + +## 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_DHT` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_DHT_DHTTYPE` - DHT model: 11, 21, 22 for DHT11, DHT21, or DHT22, defaults to 22/DHT22 +* `USERMOD_DHT_PIN` - pin to which DTH is connected, defaults to Q2 pin on QuinLed Dig-Uno's board +* `USERMOD_DHT_CELSIUS` - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported +* `USERMOD_DHT_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60 seconds +* `USERMOD_DHT_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 90 seconds +* `USERMOD_DHT_STATS` - For debug, report delay stats + +## Project link + +* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link + +### PlatformIO requirements + +If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:d1_mini_usermod_dht_C`. If not, you can add the libraries and dependencies into `platformio.ini` as you see fit. + + +## Change Log + +2020-02-04 +* Change default QuinLed pin to Q2 +* Instead of trying to keep updates at constant cadence, space readings out by measurement interval; hope this helps to avoid occasional bursts of readings with errors +* Add some more (optional) stats +2020-02-03 +* Due to poor readouts on ESP32 with previous DHT library, rewrote to use https://github.com/alwynallan/DHT_nonblocking +* The new library serializes/delays up to 5ms for the sensor readout +2020-02-02 +* Created diff --git a/usermods/DHT/usermod_dht.h b/usermods/DHT/usermod_dht.h new file mode 100644 index 000000000..9f46734f8 --- /dev/null +++ b/usermods/DHT/usermod_dht.h @@ -0,0 +1,216 @@ +#pragma once + +#include "wled.h" + +#include + +// USERMOD_DHT_DHTTYPE: +// 11 // DHT 11 +// 21 // DHT 21 +// 22 // DHT 22 (AM2302), AM2321 *** default +#ifndef USERMOD_DHT_DHTTYPE +#define USERMOD_DHT_DHTTYPE 22 +#endif + +#if USERMOD_DHT_DHTTYPE == 11 +#define DHTTYPE DHT_TYPE_11 +#elif USERMOD_DHT_DHTTYPE == 21 +#define DHTTYPE DHT_TYPE_21 +#elif USERMOD_DHT_DHTTYPE == 22 +#define DHTTYPE DHT_TYPE_22 +#endif + +// Connect pin 1 (on the left) of the sensor to +5V +// NOTE: If using a board with 3.3V logic like an Arduino Due connect pin 1 +// to 3.3V instead of 5V! +// Connect pin 2 of the sensor to whatever your DHTPIN is +// NOTE: Pin defaults below are for QuinLed Dig-Uno's Q2 on the board +// Connect pin 4 (on the right) of the sensor to GROUND +// NOTE: If using a bare sensor (AM*), Connect a 10K resistor from pin 2 +// (data) to pin 1 (power) of the sensor. DHT* boards have the pullup already + +#ifdef USERMOD_DHT_PIN +#define DHTPIN USERMOD_DHT_PIN +#else +#ifdef ARDUINO_ARCH_ESP32 +#define DHTPIN 21 +#else //ESP8266 boards +#define DHTPIN 4 +#endif +#endif + +// the frequency to check sensor, 1 minute +#ifndef USERMOD_DHT_MEASUREMENT_INTERVAL +#define USERMOD_DHT_MEASUREMENT_INTERVAL 60000 +#endif + +// how many seconds after boot to take first measurement, 90 seconds +// 90 gives enough time to OTA update firmware if this crashses +#ifndef USERMOD_DHT_FIRST_MEASUREMENT_AT +#define USERMOD_DHT_FIRST_MEASUREMENT_AT 90000 +#endif + +// from COOLDOWN_TIME in dht_nonblocking.cpp +#define DHT_TIMEOUT_TIME 10000 + +DHT_nonblocking dht_sensor(DHTPIN, DHTTYPE); + +class UsermodDHT : public Usermod { + private: + unsigned long nextReadTime = 0; + unsigned long lastReadTime = 0; + float humidity, temperature = 0; + bool initializing = true; + bool disabled = false; + #ifdef USERMOD_DHT_STATS + unsigned long nextResetStatsTime = 0; + uint16_t updates = 0; + uint16_t clean_updates = 0; + uint16_t errors = 0; + unsigned long maxDelay = 0; + unsigned long currentIteration = 0; + unsigned long maxIteration = 0; + #endif + + public: + void setup() { + nextReadTime = millis() + USERMOD_DHT_FIRST_MEASUREMENT_AT; + lastReadTime = millis(); + #ifdef USERMOD_DHT_STATS + nextResetStatsTime = millis() + 60*60*1000; + #endif + } + + void loop() { + if (disabled) { + return; + } + if (millis() < nextReadTime) { + return; + } + + #ifdef USERMOD_DHT_STATS + if (millis() >= nextResetStatsTime) { + nextResetStatsTime += 60*60*1000; + errors = 0; + updates = 0; + clean_updates = 0; + } + unsigned long dcalc = millis(); + if (currentIteration == 0) { + currentIteration = millis(); + } + #endif + + float tempC; + if (dht_sensor.measure(&tempC, &humidity)) { + #ifdef USERMOD_DHT_CELSIUS + temperature = tempC; + #else + temperature = tempC * 9 / 5 + 32; + #endif + + nextReadTime = millis() + USERMOD_DHT_MEASUREMENT_INTERVAL; + lastReadTime = millis(); + initializing = false; + + #ifdef USERMOD_DHT_STATS + unsigned long icalc = millis() - currentIteration; + if (icalc > maxIteration) { + maxIteration = icalc; + } + if (icalc > DHT_TIMEOUT_TIME) { + errors += icalc/DHT_TIMEOUT_TIME; + } else { + clean_updates += 1; + } + updates += 1; + currentIteration = 0; + + #endif + } + + #ifdef USERMOD_DHT_STATS + dcalc = millis() - dcalc; + if (dcalc > maxDelay) { + maxDelay = dcalc; + } + #endif + + if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) { + disabled = true; + } + } + + void addToJsonInfo(JsonObject& root) { + if (disabled) { + return; + } + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray temp = user.createNestedArray("Temperature"); + JsonArray hum = user.createNestedArray("Humidity"); + + #ifdef USERMOD_DHT_STATS + JsonArray next = user.createNestedArray("next"); + if (nextReadTime >= millis()) { + next.add((nextReadTime - millis()) / 1000); + next.add(" sec until read"); + } else { + next.add((millis() - nextReadTime) / 1000); + next.add(" sec active reading"); + } + + JsonArray last = user.createNestedArray("last"); + last.add((millis() - lastReadTime) / 60000); + last.add(" min since read"); + + JsonArray err = user.createNestedArray("errors"); + err.add(errors); + err.add(" Errors"); + + JsonArray upd = user.createNestedArray("updates"); + upd.add(updates); + upd.add(" Updates"); + + JsonArray cupd = user.createNestedArray("cleanUpdates"); + cupd.add(clean_updates); + cupd.add(" Updates"); + + JsonArray iter = user.createNestedArray("maxIter"); + iter.add(maxIteration); + iter.add(" ms"); + + JsonArray delay = user.createNestedArray("maxDelay"); + delay.add(maxDelay); + delay.add(" ms"); + #endif + + if (initializing) { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + temp.add((nextReadTime - millis()) / 1000); + temp.add(" sec until read"); + hum.add((nextReadTime - millis()) / 1000); + hum.add(" sec until read"); + return; + } + + hum.add(humidity); + hum.add("%"); + + temp.add(temperature); + #ifdef USERMOD_DHT_CELSIUS + temp.add("°C"); + #else + temp.add("°F"); + #endif + } + + uint16_t getId() + { + return USERMOD_ID_DHT; + } + +}; diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md new file mode 100644 index 000000000..5c835c60e --- /dev/null +++ b/usermods/usermod_v2_auto_save/readme.md @@ -0,0 +1,45 @@ +# Auto Save + +v2 Usermod to automatically save settings +to preset number AUTOSAVE_PRESET_NUM after a change to any of + +* brightness +* effect speed +* effect intensity +* mode (effect) +* palette + +but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" +period in case there are other changes (any change will +extend the "settle" window). + +It will additionally load preset AUTOSAVE_PRESET_NUM at startup. +during the first `loop()`. Reasoning below. + +AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. + +Note: I don't love that WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed. + +## Installation + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermode folder to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_AUTO_SAVE` - define this to have this the Auto Save usermod included wled00\usermods_list.cpp +* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) +* `AUTOSAVE_SETTLE_MS` - Minimum time to wave before auto saving, defaults to 10000 (10s) +* `AUTOSAVE_PRESET_NUM` - Preset number to auto-save to, auto-load at startup from, defaults to 99 + +### PlatformIO requirements + +No special requirements. + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-02 +* First public release diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h new file mode 100644 index 000000000..bd7ea6d81 --- /dev/null +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -0,0 +1,192 @@ +#pragma once + +#include "wled.h" + +// +// v2 Usermod to automatically save settings +// to preset number AUTOSAVE_PRESET_NUM after a change to any of +// +// * brightness +// * effect speed +// * effect intensity +// * mode (effect) +// * palette +// +// but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" +// period in case there are other changes (any change will +// extend the "settle" window). +// +// It will additionally load preset AUTOSAVE_PRESET_NUM at startup. +// during the first `loop()`. Reasoning below. +// +// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod +// is installed, it will notify the user of the saved changes. +// +// Note: I don't love that WLED doesn't respect the brightness +// of the preset being auto loaded, so the AutoSaveUsermod +// will set the AUTOSAVE_PRESET_NUM preset in the first loop, +// so brightness IS honored. This means WLED will effectively +// ignore Default brightness and Apply N preset at boot when +// the AutoSaveUsermod is installed. + +//How long to wait after settings change to auto-save +#ifndef AUTOSAVE_SETTLE_MS +#define AUTOSAVE_SETTLE_MS 10*1000 +#endif + +//Preset number to save to +#ifndef AUTOSAVE_PRESET_NUM +#define AUTOSAVE_PRESET_NUM 99 +#endif + +// "Auto save MM-DD HH:MM:SS" +#define PRESET_NAME_BUFFER_SIZE 25 + +class AutoSaveUsermod : public Usermod { + private: + // If we've detected the need to auto save, this will + // be non zero. + unsigned long autoSaveAfter = 0; + + char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; + + bool firstLoop = true; + + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + +#ifdef USERMOD_FOUR_LINE_DISLAY + FourLineDisplayUsermod* display; +#endif + + public: + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { +#ifdef USERMOD_FOUR_LINE_DISLAY + // This Usermod has enhanced funcionality if + // FourLineDisplayUsermod is available. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); +#endif + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /** + * Da loop. + */ + void loop() { + unsigned long now = millis(); + uint8_t currentMode = strip.getMode(); + uint8_t currentPalette = strip.getSegment(0).palette; + if (firstLoop) { + firstLoop = false; + applyPreset(AUTOSAVE_PRESET_NUM); + knownBrightness = bri; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownMode = currentMode; + knownPalette = currentPalette; + return; + } + + unsigned long wouldAutoSaveAfter = now + AUTOSAVE_SETTLE_MS; + if (knownBrightness != bri) { + knownBrightness = bri; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectSpeed != effectSpeed) { + knownEffectSpeed = effectSpeed; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectIntensity != effectIntensity) { + knownEffectIntensity = effectIntensity; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownMode != currentMode) { + knownMode = currentMode; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownPalette != currentPalette) { + knownPalette = currentPalette; + autoSaveAfter = wouldAutoSaveAfter; + } + + if (autoSaveAfter && now > autoSaveAfter) { + autoSaveAfter = 0; + // Time to auto save. You may have some flickry? + saveSettings(); + displayOverlay(); + } + } + + void saveSettings() { + updateLocalTime(); + sprintf(presetNameBuffer, + "Auto save %02d-%02d %02d:%02d:%02d", + month(localTime), day(localTime), + hour(localTime), minute(localTime), second(localTime)); + savePreset(AUTOSAVE_PRESET_NUM, true, presetNameBuffer); + } + + void displayOverlay() { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display != nullptr) { + display->wakeDisplay(); + display->overlay("Settings", "Auto Saved", 1500); + } +#endif + } + + /* + * 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) { + } + + /* + * 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) { + } + + /* + * 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 :) + */ + void readFromConfig(JsonObject& root) { + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_AUTO_SAVE; + } + +}; \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display/readme.md b/usermods/usermod_v2_four_line_display/readme.md new file mode 100644 index 000000000..3198b2be5 --- /dev/null +++ b/usermods/usermod_v2_four_line_display/readme.md @@ -0,0 +1,39 @@ +# Rotary Encoder UI Usermod + +First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. + +This usermod provides a four line display using either +128x32 or 128x64 OLED displays. +It's can operate independently, but starts to provide +a relatively complete on-device UI when paired with the +Rotary Encoder UI usermod. I strongly encourage you to use +them together. + +[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) + +## Installation + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermode folder to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available +* `FLD_PIN_SCL` - The display SCL pin, defaults to 5 +* `FLD_PIN_SDA` - The display SDA pin, defaults to 4 +* `FLIP_MODE` - Set to 0 or 1 +* `LINE_HEIGHT` - Set to 1 or 2 + +There are other `#define` values in the Usermod that might be of interest. + +### PlatformIO requirements + +This usermod requires the `U8g2` and `Wire` libraries. See the +`platformio_override.ini.sample` found in the Rotary Encoder +UI usermod folder for how to include these using `platformio_override.ini`. + +## Change Log + +2021-02 +* First public release diff --git a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h new file mode 100644 index 000000000..9f4716218 --- /dev/null +++ b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h @@ -0,0 +1,530 @@ +#pragma once + +#include "wled.h" +#include // from https://github.com/olikraus/u8g2/ + +// +// Insired by the v1 usermod: ssd1306_i2c_oled_u8g2 +// +// v2 usermod for using 128x32 or 128x64 i2c +// OLED displays to provide a four line display +// for WLED. +// +// 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. +#ifndef FLD_PIN_SCL +#define FLD_PIN_SCL 5 +#endif + +#ifndef FLD_PIN_SDA +#define FLD_PIN_SDA 4 +#endif + +// U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8( +// U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA); +U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8( + U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA); + +// Screen upside down? Change to 0 or 1 +#ifndef FLIP_MODE +#define FLIP_MODE 0 +#endif + +// LINE_HEIGHT 1 is single height, for 128x32 displays. +// LINE_HEIGHT 2 makes the 128x64 screen display at double height. +#ifndef LINE_HEIGHT +#define LINE_HEIGHT 2 +#endif + +// If you aren't also including RotaryEncoderUIUsermod +// you probably want to set both +// SLEEP_MODE_ENABLED false +// CLOCK_MODE_ENABLED false +// as you will never be able wake the display / disable the clock. +#ifdef USERMOD_ROTARY_ENCODER_UI +#ifndef SLEEP_MODE_ENABLED +#define SLEEP_MODE_ENABLED true +#endif +#ifndef CLOCK_MODE_ENABLED +#define CLOCK_MODE_ENABLED true +#endif +#else +#define SLEEP_MODE_ENABLED false +#define CLOCK_MODE_ENABLED false +#endif + +// When to time out to the clock or blank the screen +// if SLEEP_MODE_ENABLED. +#define SCREEN_TIMEOUT_MS 15*1000 + +#define TIME_INDENT 0 +#define DATE_INDENT 2 + +// Minimum time between redrawing screen in ms +#define USER_LOOP_REFRESH_RATE_MS 1000 + +#if LINE_HEIGHT == 2 +#define DRAW_STRING draw1x2String +#define DRAW_GLYPH draw1x2Glyph +#define DRAW_BIG_STRING draw2x2String +#else +#define DRAW_STRING drawString +#define DRAW_GLYPH drawGlyph +#define DRAW_BIG_STRING draw2x2String +#endif + +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 16+1 +#define FLD_LINE_3_BRIGHTNESS 0 +#define FLD_LINE_3_EFFECT_SPEED 1 +#define FLD_LINE_3_EFFECT_INTENSITY 2 +#define FLD_LINE_3_PALETTE 3 + +#if LINE_HEIGHT == 2 +#define TIME_LINE 1 +#else +#define TIME_LINE 0 +#endif + +class FourLineDisplayUsermod : public Usermod { + private: + unsigned long lastTime = 0; + + // 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; + uint8_t knownHour = 99; + + bool displayTurnedOff = false; + long lastUpdate = 0; + long lastRedraw = 0; + long overlayUntil = 0; + byte lineThreeType = FLD_LINE_3_BRIGHTNESS; + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 0; + + char lineBuffer[LINE_BUFFER_SIZE]; + + // If display does not work or looks corrupted check the + // constructor reference: + // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp + // or check the gallery: + // https://github.com/olikraus/u8g2/wiki/gallery + public: + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { + u8x8.begin(); + u8x8.setFlipMode(FLIP_MODE); + u8x8.setPowerSave(0); + u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 + u8x8.setFont(u8x8_font_chroma48medium8_r); + u8x8.DRAW_STRING(0, 0*LINE_HEIGHT, "Loading..."); + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /** + * Da loop. + */ + void loop() { + if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { + return; + } + lastUpdate = millis(); + + redraw(false); + } + + /** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ + void redraw(bool forceRedraw) { + if (overlayUntil > 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 (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) { + needRedraw = true; + } else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) { + needRedraw = true; + } else if (knownBrightness != bri) { + needRedraw = true; + } else if (knownEffectSpeed != effectSpeed) { + needRedraw = true; + } else if (knownEffectIntensity != effectIntensity) { + needRedraw = true; + } else if (knownMode != strip.getMode()) { + needRedraw = true; + } else if (knownPalette != strip.getSegment(0).palette) { + needRedraw = true; + } + + if (!needRedraw) { + // Nothing to change. + // Turn off display after 3 minutes with no change. + if(SLEEP_MODE_ENABLED && !displayTurnedOff && + (millis() - lastRedraw > SCREEN_TIMEOUT_MS)) { + // We will still check if there is a change in redraw() + // and turn it back on if it changed. + sleepOrClock(true); + } + else if (displayTurnedOff && CLOCK_MODE_ENABLED) { + showTime(); + } + return; + } + needRedraw = false; + lastRedraw = millis(); + + if (displayTurnedOff) + { + // Turn the display back on + sleepOrClock(false); + } + + // Update last known values. + #if defined(ESP8266) + knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); + #else + knownSsid = WiFi.SSID(); + #endif + knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); + knownBrightness = bri; + knownMode = strip.getMode(); + knownPalette = strip.getSegment(0).palette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + + // Do the actual drawing + u8x8.clear(); + u8x8.setFont(u8x8_font_chroma48medium8_r); + + // First row with Wifi name + String ssidString = knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0); + u8x8.DRAW_STRING(1, 0*LINE_HEIGHT, ssidString.c_str()); + // Print `~` char to indicate that SSID is longer, than owr dicplay + if (knownSsid.length() > u8x8.getCols()) { + u8x8.DRAW_STRING(u8x8.getCols() - 1, 0*LINE_HEIGHT, "~"); + } + + // Second row with IP or Psssword + // Print password in AP mode and if led is OFF. + if (apActive && bri == 0) { + u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, apPass); + } + else { + String ipString = knownIp.toString(); + u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, ipString.c_str()); + } + + // Third row with mode name + showCurrentEffectOrPalette(JSON_mode_names, 2, knownMode); + + switch(lineThreeType) { + case FLD_LINE_3_BRIGHTNESS: + sprintf(lineBuffer, "Brightness %d", bri); + u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + break; + case FLD_LINE_3_EFFECT_SPEED: + sprintf(lineBuffer, "FX Speed %d", effectSpeed); + u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + break; + case FLD_LINE_3_EFFECT_INTENSITY: + sprintf(lineBuffer, "FX Intense %d", effectIntensity); + u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + break; + case FLD_LINE_3_PALETTE: + showCurrentEffectOrPalette(JSON_palette_names, 3, knownPalette); + break; + } + + u8x8.setFont(u8x8_font_open_iconic_arrow_1x1); + u8x8.DRAW_GLYPH(0, markLineNum*LINE_HEIGHT, 66); // arrow icon + + u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); + u8x8.DRAW_GLYPH(0, 0*LINE_HEIGHT, 80); // wifi icon + u8x8.DRAW_GLYPH(0, 1*LINE_HEIGHT, 68); // home icon + } + + /** + * Display the current effect or palette (desiredEntry) + * on the appropriate line (row). + * + * TODO: Should we cache the current effect and + * TODO: palette name? This seems expensive. + */ + void showCurrentEffectOrPalette(const char json[], uint8_t row, uint8_t desiredEntry) { + uint8_t qComma = 0; + bool insideQuotes = false; + // advance past the mark for markLineNum that may exist. + uint8_t printedChars = 1; + char singleJsonSymbol; + + // Find the mode name in JSON + for (size_t i = 0; i < strlen_P(json); i++) { + singleJsonSymbol = pgm_read_byte_near(json + i); + switch (singleJsonSymbol) { + case '"': + insideQuotes = !insideQuotes; + break; + case '[': + case ']': + break; + case ',': + qComma++; + default: + if (!insideQuotes || (qComma != desiredEntry)) { + break; + } + u8x8.DRAW_GLYPH(printedChars, row * LINE_HEIGHT, singleJsonSymbol); + printedChars++; + } + if ((qComma > desiredEntry) || (printedChars > u8x8.getCols() - 2)) { + break; + } + } + } + + /** + * 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() { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + redraw(true); + return true; + } + return false; + } + + /** + * Allows you to show up to two lines as overlay for a + * period of time. + * Clears the screen and prints on the middle two lines. + */ + void overlay(const char* line1, const char *line2, long showHowLong) { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + } + + // Print the overlay + u8x8.clear(); + u8x8.setFont(u8x8_font_chroma48medium8_r); + if (line1) { + u8x8.DRAW_STRING(0, 1*LINE_HEIGHT, line1); + } + if (line2) { + u8x8.DRAW_STRING(0, 2*LINE_HEIGHT, line2); + } + overlayUntil = millis() + showHowLong; + } + + /** + * Specify what data should be defined on line 3 + * (the last line). + */ + void setLineThreeType(byte newLineThreeType) { + if (newLineThreeType == FLD_LINE_3_BRIGHTNESS || + newLineThreeType == FLD_LINE_3_EFFECT_SPEED || + newLineThreeType == FLD_LINE_3_EFFECT_INTENSITY || + newLineThreeType == FLD_LINE_3_PALETTE) { + lineThreeType = newLineThreeType; + } + else { + // Unknown value. + lineThreeType = FLD_LINE_3_BRIGHTNESS; + } + } + + /** + * Line 2 or 3 (last two lines) can be marked with an + * arrow in the first column. Pass 2 or 3 to this to + * specify which line to mark with an arrow. + * Any other values are ignored. + */ + void setMarkLine(byte newMarkLineNum) { + if (newMarkLineNum == 2 || newMarkLineNum == 3) { + markLineNum = newMarkLineNum; + } + else { + markLineNum = 0; + } + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + /* + void addToJsonInfo(JsonObject& root) + { + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit + } + */ + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled) { + if (enabled) { + if (CLOCK_MODE_ENABLED) { + showTime(); + } + else { + u8x8.setPowerSave(1); + } + displayTurnedOff = true; + } + else { + if (!CLOCK_MODE_ENABLED) { + u8x8.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() { + updateLocalTime(); + byte minuteCurrent = minute(localTime); + byte hourCurrent = hour(localTime); + if (knownMinute == minuteCurrent && knownHour == hourCurrent) { + // Time hasn't changed. + return; + } + knownMinute = minuteCurrent; + knownHour = hourCurrent; + + u8x8.clear(); + u8x8.setFont(u8x8_font_chroma48medium8_r); + + int currentMonth = month(localTime); + sprintf(lineBuffer, "%s %d", monthShortStr(currentMonth), day(localTime)); + u8x8.DRAW_BIG_STRING(DATE_INDENT, TIME_LINE*LINE_HEIGHT, lineBuffer); + + byte showHour = hourCurrent; + boolean isAM = false; + if (useAMPM) { + if (showHour == 0) { + showHour = 12; + isAM = true; + } + else if (showHour > 12) { + showHour -= 12; + isAM = false; + } + else { + isAM = true; + } + } + + sprintf(lineBuffer, "%02d:%02d %s", showHour, minuteCurrent, useAMPM ? (isAM ? "AM" : "PM") : ""); + // For time, we always use LINE_HEIGHT of 2 since + // we are printing it big. + u8x8.DRAW_BIG_STRING(TIME_INDENT + (useAMPM ? 0 : 2), (TIME_LINE + 1) * 2, lineBuffer); + } + + /* + * 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) { + } + + /* + * 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) { + } + + /* + * 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 :) + */ + void readFromConfig(JsonObject& root) { + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_FOUR_LINE_DISP; + } + +}; \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample new file mode 100644 index 000000000..485d67f6a --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample @@ -0,0 +1,46 @@ +[platformio] +default_envs = d1_mini +; default_envs = esp32dev + +[env:esp32dev] +board = esp32dev +platform = espressif32@2.0 +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp32} + -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19 + -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 + -D LEDPIN=16 -D BTNPIN=13 +upload_speed = 460800 +lib_ignore = + ESPAsyncTCP + ESPAsyncUDP + +[env:d1_mini] +board = d1_mini +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +upload_speed = 460800 +board_build.ldscript = ${common.ldscript_4m1m} +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=12 -D ENCODER_CLK_PIN=14 -D ENCODER_SW_PIN=13 + -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 + -D LEDPIN=3 -D BTNPIN=0 +monitor_filters = esp8266_exception_decoder + +[env] +lib_deps = + fastled/FastLED @ 3.3.2 + NeoPixelBus @ 2.6.0 + ESPAsyncTCP @ 1.2.0 + ESPAsyncUDP + AsyncTCP @ 1.0.3 + IRremoteESP8266 @ 2.7.3 + https://github.com/lorol/LITTLEFS.git + https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.0 + U8g2@~2.27.2 + Wire diff --git a/usermods/usermod_v2_rotary_encoder_ui/readme.md b/usermods/usermod_v2_rotary_encoder_ui/readme.md new file mode 100644 index 000000000..4477590b5 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/readme.md @@ -0,0 +1,33 @@ +# Rotary Encoder UI Usermod + +First, thanks to the authors of other Rotary Encoder usermods. + +This usermod starts to provide a relatively complete on-device +UI when paired with the Four Line Display usermod. I strongly +encourage you to try them together. + +[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) + +## Installation + +Copy and update the example `platformio_override.ini.sample` to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) +* `ENCODER_DT_PIN` - The encoders DT pin, defaults to 12 +* `ENCODER_CLK_PIN` - The encoders CLK pin, defaults to 14 +* `ENCODER_SW_PIN` - The encoders SW pin, defaults to 13 + +### PlatformIO requirements + +No special requirements. + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-02 +* First public release diff --git a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h new file mode 100644 index 000000000..59cabfc4d --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h @@ -0,0 +1,420 @@ +#pragma once + +#include "wled.h" + +// +// Inspired by the v1 usermods +// * rotary_encoder_change_brightness +// * rotary_encoder_change_effect +// +// v2 usermod that provides a rotary encoder-based UI. +// +// This Usermod works best coupled with FourLineDisplayUsermod. +// +// This usermod allows you to control: +// +// * Brightness +// * Selected Effect +// * Effect Speed +// * Effect Intensity +// * Palette +// +// Change between modes by pressing a button. +// + +#ifndef ENCODER_DT_PIN +#define ENCODER_DT_PIN 12 +#endif + +#ifndef ENCODER_CLK_PIN +#define ENCODER_CLK_PIN 14 +#endif + +#ifndef ENCODER_SW_PIN +#define ENCODER_SW_PIN 13 +#endif + +#ifndef USERMOD_FOUR_LINE_DISLAY +// These constants won't be defined if we aren't using FourLineDisplay. +#define FLD_LINE_3_BRIGHTNESS 0 +#define FLD_LINE_3_EFFECT_SPEED 0 +#define FLD_LINE_3_EFFECT_INTENSITY 0 +#define FLD_LINE_3_PALETTE 0 +#endif + +// The last UI state +#define LAST_UI_STATE 4 + +/** + * Array of mode indexes in alphabetical order. + * Should be ordered from JSON_mode_names array in FX.h. + * + * NOTE: If JSON_mode_names changes, this will need to be updated. + */ +const byte modes_alpha_order[] = { + 0, 27, 38, 115, 1, 26, 91, 68, 2, 88, 102, 114, 28, 31, 32, + 30, 29, 111, 52, 34, 8, 74, 67, 112, 18, 19, 96, 7, 117, 12, + 69, 66, 45, 42, 90, 89, 110, 87, 46, 53, 82, 100, 58, 64, 75, + 41, 57, 47, 44, 76, 77, 59, 70, 71, 72, 73, 107, 62, 101, 65, + 98, 105, 109, 97, 48, 49, 95, 63, 78, 43, 9, 33, 5, 79, 99, + 15, 37, 16, 10, 11, 40, 60, 108, 92, 93, 94, 103, 83, 84, 20, + 21, 22, 85, 86, 39, 61, 23, 25, 24, 104, 6, 36, 13, 14, 35, + 54, 56, 55, 116, 17, 81, 80, 106, 51, 50, 113, 3, 4 }; + +/** + * Array of palette indexes in alphabetical order. + * Should be ordered from JSON_palette_names array in FX.h. + * + * NOTE: If JSON_palette_names changes, this will need to be updated. + */ +const byte palettes_alpha_order[] = { + 0, 1, 2, 3, 4, 5, 18, 46, 51, 50, 55, 39, 26, 22, 15, + 48, 52, 53, 7, 37, 24, 30, 35, 10, 32, 28, 29, 36, 31, + 25, 8, 38, 40, 41, 9, 44, 47, 6, 20, 11, 12, 16, 33, + 14, 49, 27, 19, 13, 21, 54, 34, 45, 23, 43, 17, 42 }; + +class RotaryEncoderUIUsermod : public Usermod { +private: + int fadeAmount = 10; // Amount to change every step (brightness) + unsigned long currentTime; + unsigned long loopTime; + const int pinA = ENCODER_DT_PIN; // DT from encoder + const int pinB = ENCODER_CLK_PIN; // CLK from encoder + const int 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; + +#ifdef USERMOD_FOUR_LINE_DISLAY + FourLineDisplayUsermod* display; +#else + void* display = nullptr; +#endif + unsigned char Enc_A; + unsigned char Enc_B; + unsigned char Enc_A_prev = 0; + + bool currentEffectAndPaleeteInitialized = false; + uint8_t effectCurrentIndex = 0; + uint8_t effectPaletteIndex = 0; + +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() + { + pinMode(pinA, INPUT_PULLUP); + pinMode(pinB, INPUT_PULLUP); + pinMode(pinC, INPUT_PULLUP); + currentTime = millis(); + loopTime = currentTime; + +#ifdef USERMOD_FOUR_LINE_DISLAY + // 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->setLineThreeType(FLD_LINE_3_BRIGHTNESS); + display->setMarkLine(3); + } +#endif + } + + /* + * 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 (!currentEffectAndPaleeteInitialized) { + findCurrentEffectAndPalette(); + } + + if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz + { + button_state = digitalRead(pinC); + if (prev_button_state != button_state) + { + if (button_state == LOW) + { + 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", FLD_LINE_3_BRIGHTNESS, 3); + break; + case 1: + changedState = changeState("Select FX", FLD_LINE_3_EFFECT_SPEED, 2); + break; + case 2: + changedState = changeState("FX Speed", FLD_LINE_3_EFFECT_SPEED, 3); + break; + case 3: + changedState = changeState("FX Intensity", FLD_LINE_3_EFFECT_INTENSITY, 3); + break; + case 4: + changedState = changeState("Palette", FLD_LINE_3_PALETTE, 3); + break; + } + } + if (changedState) { + select_state = newState; + } + } + else + { + prev_button_state = button_state; + } + } + int Enc_A = digitalRead(pinA); // Read encoder pins + int Enc_B = digitalRead(pinB); + if ((!Enc_A) && (Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == HIGH) + { // B is high so clockwise + switch(select_state) { + case 0: + changeBrightness(true); + break; + case 1: + changeEffect(true); + break; + case 2: + changeEffectSpeed(true); + break; + case 3: + changeEffectIntensity(true); + break; + case 4: + changePalette(true); + break; + } + } + else if (Enc_B == LOW) + { // B is low so counter-clockwise + switch(select_state) { + case 0: + changeBrightness(false); + break; + case 1: + changeEffect(false); + break; + case 2: + changeEffectSpeed(false); + break; + case 3: + changeEffectIntensity(false); + break; + case 4: + changePalette(false); + break; + } + } + } + Enc_A_prev = Enc_A; // Store value of A for next time + loopTime = currentTime; // Updates loopTime + } + } + + void findCurrentEffectAndPalette() { + currentEffectAndPaleeteInitialized = true; + for (uint8_t i = 0; i < strip.getModeCount(); i++) { + byte value = modes_alpha_order[i]; + if (modes_alpha_order[i] == effectCurrent) { + effectCurrentIndex = i; + break; + } + } + + for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { + byte value = palettes_alpha_order[i]; + if (palettes_alpha_order[i] == strip.getSegment(0).palette) { + effectPaletteIndex = i; + break; + } + } + } + + boolean changeState(const char *stateName, byte lineThreeMode, byte markedLine) { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display != nullptr) { + if (display->wakeDisplay()) { + // Throw away wake up input + return false; + } + display->overlay("Mode change", stateName, 1500); + display->setLineThreeType(lineThreeMode); + display->setMarkLine(markedLine); + } + #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(NOTIFIER_CALL_MODE_DIRECT_CHANGE); + updateInterfaces(NOTIFIER_CALL_MODE_DIRECT_CHANGE); + } + + void changeBrightness(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + 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(); + } + + void changeEffect(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + 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_order[effectCurrentIndex]; + lampUdated(); + } + + void changeEffectSpeed(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + 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(); + } + + void changeEffectIntensity(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + 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(); + } + + void changePalette(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + 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_order[effectPaletteIndex]; + lampUdated(); + } + + /* + * 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!")); + } + + /* + * 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; + } + + //More methods can be added in the future, this example will then be extended. + //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! +}; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6852674e4..3fdfcff1e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1001,14 +1001,6 @@ uint16_t WS2812FX::mode_running_color(void) { return running(SEGCOLOR(0), SEGCOLOR(1)); } - -/* - * Alternating red/green pixels running. - */ -uint16_t WS2812FX::mode_merry_christmas(void) { - return running(RED, GREEN); -} - /* * Alternating red/white pixels running. */ @@ -1991,7 +1983,7 @@ uint16_t WS2812FX::mode_colortwinkle() if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed CRGB fastled_col, prev; - fract8 fadeUpAmount = 8 + (SEGMENT.speed/4), fadeDownAmount = 5 + (SEGMENT.speed/7); + fract8 fadeUpAmount = _brightness>28 ? 8 + (SEGMENT.speed>>2) : 68-_brightness, fadeDownAmount = _brightness>28 ? 8 + (SEGMENT.speed>>3) : 68-_brightness; for (uint16_t i = 0; i < SEGLEN; i++) { fastled_col = col_to_crgb(getPixelColor(i)); prev = fastled_col; @@ -3144,6 +3136,59 @@ uint16_t WS2812FX::mode_drip(void) } +/* + * Tetris or Stacking (falling bricks) Effect + * by Blaz Kristan (https://github.com/blazoncek, https://blaz.at/home) + */ +typedef struct Tetris { + float pos; + float speed; + uint32_t col; +} tetris; + +uint16_t WS2812FX::mode_tetrix(void) { + + uint16_t dataSize = sizeof(tetris); + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + Tetris* drop = reinterpret_cast(SEGENV.data); + + // initialize dropping on first call or segment full + if (SEGENV.call == 0 || SEGENV.aux1 >= SEGLEN) { + SEGENV.aux1 = 0; // reset brick stack size + SEGENV.step = 0; + fill(SEGCOLOR(1)); + return 250; // short wait + } + + if (SEGENV.step == 0) { //init + drop->speed = 0.0238 * (SEGMENT.speed ? (SEGMENT.speed>>3)+1 : random8(6,40)); // set speed + drop->pos = SEGLEN-1; // start at end of segment + drop->col = color_from_palette(random8(0,15)<<4,false,false,0); // limit color choices so there is enough HUE gap + SEGENV.step = 1; // drop state (0 init, 1 forming, 2 falling) + SEGENV.aux0 = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : random8(1,5)) * (1+(SEGLEN>>6)); // size of brick + } + + if (SEGENV.step == 1) { // forming + if (random8()>>6) { // random drop + SEGENV.step = 2; // fall + } + } + + if (SEGENV.step > 1) { // falling + if (drop->pos > SEGENV.aux1) { // fall until top of stack + drop->pos -= drop->speed; // may add gravity as: speed += gravity + if (int(drop->pos) < SEGENV.aux1) drop->pos = SEGENV.aux1; + for (uint16_t i=int(drop->pos); ipos)+SEGENV.aux0 ? drop->col : SEGCOLOR(1)); + } else { // we hit bottom + SEGENV.step = 0; // go back to init + SEGENV.aux1 += SEGENV.aux0; // increase the stack size + if (SEGENV.aux1 >= SEGLEN) return 1000; // wait for a second + } + } + return FRAMETIME; +} + + /* / Plasma Effect / adapted from https://github.com/atuline/FastLED-Demos/blob/master/plasma/plasma.ino diff --git a/wled00/FX.h b/wled00/FX.h index ba2fbc6e8..b681a0793 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -160,7 +160,7 @@ #define FX_MODE_COMET 41 #define FX_MODE_FIREWORKS 42 #define FX_MODE_RAIN 43 -#define FX_MODE_MERRY_CHRISTMAS 44 +#define FX_MODE_TETRIX 44 #define FX_MODE_FIRE_FLICKER 45 #define FX_MODE_GRADIENT 46 #define FX_MODE_LOADING 47 @@ -497,7 +497,7 @@ class WS2812FX { _mode[FX_MODE_COMET] = &WS2812FX::mode_comet; _mode[FX_MODE_FIREWORKS] = &WS2812FX::mode_fireworks; _mode[FX_MODE_RAIN] = &WS2812FX::mode_rain; - _mode[FX_MODE_MERRY_CHRISTMAS] = &WS2812FX::mode_merry_christmas; + _mode[FX_MODE_TETRIX] = &WS2812FX::mode_tetrix; _mode[FX_MODE_FIRE_FLICKER] = &WS2812FX::mode_fire_flicker; _mode[FX_MODE_GRADIENT] = &WS2812FX::mode_gradient; _mode[FX_MODE_LOADING] = &WS2812FX::mode_loading; @@ -721,7 +721,7 @@ class WS2812FX { mode_comet(void), mode_fireworks(void), mode_rain(void), - mode_merry_christmas(void), + mode_tetrix(void), mode_halloween(void), mode_fire_flicker(void), mode_gradient(void), @@ -850,7 +850,11 @@ class WS2812FX { void blendPixelColor(uint16_t n, uint32_t color, uint8_t blend), - startTransition(uint8_t oldBri, uint32_t oldCol, uint16_t dur, uint8_t segn, uint8_t slot); + startTransition(uint8_t oldBri, uint32_t oldCol, uint16_t dur, uint8_t segn, uint8_t slot), + deserializeMap(void); + + uint16_t* customMappingTable = nullptr; + uint16_t customMappingSize = 0; uint32_t _lastPaletteChange = 0; uint32_t _lastShow = 0; @@ -881,7 +885,7 @@ const char JSON_mode_names[] PROGMEM = R"=====([ "Scan","Scan Dual","Fade","Theater","Theater Rainbow","Running","Saw","Twinkle","Dissolve","Dissolve Rnd", "Sparkle","Sparkle Dark","Sparkle+","Strobe","Strobe Rainbow","Strobe Mega","Blink Rainbow","Android","Chase","Chase Random", "Chase Rainbow","Chase Flash","Chase Flash Rnd","Rainbow Runner","Colorful","Traffic Light","Sweep Random","Running 2","Aurora","Stream", -"Scanner","Lighthouse","Fireworks","Rain","Merry Christmas","Fire Flicker","Gradient","Loading","Police","Police All", +"Scanner","Lighthouse","Fireworks","Rain","Tetrix","Fire Flicker","Gradient","Loading","Police","Police All", "Two Dots","Two Areas","Circus","Halloween","Tri Chase","Tri Wipe","Tri Fade","Lightning","ICU","Multi Comet", "Scanner Dual","Stream 2","Oscillate","Pride 2015","Juggle","Palette","Fire 2012","Colorwaves","Bpm","Fill Noise", "Noise 1","Noise 2","Noise 3","Noise 4","Colortwinkles","Lake","Meteor","Meteor Smooth","Railway","Ripple", diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 2a6537df0..aa1f88a64 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -27,27 +27,26 @@ #include "FX.h" #include "palettes.h" -//enable custom per-LED mapping. This can allow for better effects on matrices or special displays -//#define WLED_CUSTOM_LED_MAPPING - -#ifdef WLED_CUSTOM_LED_MAPPING -//this is just an example (30 LEDs). It will first set all even, then all uneven LEDs. -const uint16_t customMappingTable[] = { - 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, - 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29}; - -//another example. Switches direction every 5 LEDs. -/*const uint16_t customMappingTable[] = { - 0, 1, 2, 3, 4, 9, 8, 7, 6, 5, 10, 11, 12, 13, 14, - 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25};*/ - -const uint16_t customMappingSize = sizeof(customMappingTable)/sizeof(uint16_t); //30 in example -#endif - #ifndef PWM_INDEX #define PWM_INDEX 0 #endif +/* + Custom per-LED mapping has moved! + + Create a file "ledmap.json" using the edit page. + + this is just an example (30 LEDs). It will first set all even, then all uneven LEDs. + {"map":[ + 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]} + + another example. Switches direction every 5 LEDs. + {"map":[ + 0, 1, 2, 3, 4, 9, 8, 7, 6, 5, 10, 11, 12, 13, 14, + 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25] +*/ + //do not call this method from system context (network callback) void WS2812FX::finalizeInit(bool supportWhite, uint16_t countPixels, bool skipFirst) { @@ -68,6 +67,8 @@ void WS2812FX::finalizeInit(bool supportWhite, uint16_t countPixels, bool skipFi BusConfig defCfg = BusConfig(TYPE_WS2812_RGB, defPin, 0, _lengthRaw, COL_ORDER_GRB); busses.add(defCfg); } + + deserializeMap(); _segments[0].start = 0; _segments[0].stop = _length; @@ -202,9 +203,7 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) int16_t indexSet = realIndex + (reversed ? -j : j); int16_t indexSetRev = indexSet; if (reverseMode) indexSetRev = REV(indexSet); - #ifdef WLED_CUSTOM_LED_MAPPING if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; - #endif if (indexSetRev >= SEGMENT.start && indexSetRev < SEGMENT.stop) { busses.setPixelColor(indexSet + skip, col); if (IS_MIRROR) { //set the corresponding mirrored pixel @@ -218,9 +217,8 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) } } else { //live data, etc. if (reverseMode) i = REV(i); - #ifdef WLED_CUSTOM_LED_MAPPING if (i < customMappingSize) i = customMappingTable[i]; - #endif + uint32_t col = ((w << 24) | (r << 16) | (g << 8) | (b)); busses.setPixelColor(i + skip, col); } @@ -499,9 +497,7 @@ uint32_t WS2812FX::getPixelColor(uint16_t i) { i = realPixelIndex(i); - #ifdef WLED_CUSTOM_LED_MAPPING if (i < customMappingSize) i = customMappingTable[i]; - #endif if (_skipFirstMode) i += LED_SKIP_AMOUNT; @@ -986,6 +982,31 @@ bool WS2812FX::segmentsAreIdentical(Segment* a, Segment* b) } +//load custom mapping table from JSON file +void WS2812FX::deserializeMap(void) { + if (!WLED_FS.exists("/ledmap.json")) return; + DynamicJsonDocument doc(JSON_BUFFER_SIZE); // full sized buffer for larger maps + + DEBUG_PRINTLN(F("Reading LED map from /ledmap.json...")); + + if (!readObjectFromFile("/ledmap.json", nullptr, &doc)) return; //if file does not exist just exit + + if (customMappingTable != nullptr) { + delete[] customMappingTable; + customMappingTable = nullptr; + customMappingSize = 0; + } + + JsonArray map = doc[F("map")]; + if (!map.isNull() && map.size()) { // not an empty map + customMappingSize = map.size(); + customMappingTable = new uint16_t[customMappingSize]; + for (uint16_t i=0; iaddToConfig(obj); } void UsermodManager::readFromConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromConfig(obj); } +/* + * Enables usermods to lookup another Usermod. + */ +Usermod* UsermodManager::lookup(uint16_t mod_id) { + for (byte i = 0; i < numMods; i++) { + if (ums[i]->getId() == mod_id) { + return ums[i]; + } + } + return nullptr; +} + bool UsermodManager::add(Usermod* um) { if (numMods >= WLED_MAX_USERMODS || um == nullptr) return false; diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index edc4ea2a7..a240baf7a 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -23,10 +23,24 @@ #include "usermod_v2_SensorsToMqtt.h" #endif + // BME280 v2 usermod. Define "USERMOD_BME280" in my_config.h #ifdef USERMOD_BME280 #include "../usermods/BME280_v2/usermod_bme280.h" #endif +#ifdef USERMOD_FOUR_LINE_DISLAY +#include "../usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h" +#endif +#ifdef USERMOD_ROTARY_ENCODER_UI +#include "../usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h" +#endif +#ifdef USERMOD_AUTO_SAVE +#include "../usermods/usermod_v2_auto_save/usermod_v2_auto_save.h" +#endif + +#ifdef USERMOD_DHT +#include "../usermods/DHT/usermod_dht.h" +#endif void registerUsermods() { @@ -50,4 +64,20 @@ void registerUsermods() #ifdef USERMOD_BME280 usermods.add(new UsermodBME280()); #endif +#endif +#ifdef USERMOD_SENSORSTOMQTT + usermods.add(new UserMod_SensorsToMQTT()); +#endif +#ifdef USERMOD_FOUR_LINE_DISLAY + usermods.add(new FourLineDisplayUsermod()); +#endif +#ifdef USERMOD_ROTARY_ENCODER_UI + usermods.add(new RotaryEncoderUIUsermod()); +#endif +#ifdef USERMOD_AUTO_SAVE + usermods.add(new AutoSaveUsermod()); +#endif +#ifdef USERMOD_DHT +usermods.add(new UsermodDHT()); +#endif } \ No newline at end of file diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index a44502485..cae23c9d2 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -235,14 +235,32 @@ void serveIndexOrWelcome(AsyncWebServerRequest *request) } } +bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request) +{ + AsyncWebHeader* header = request->getHeader("If-None-Match"); + if (header && header->value() == String(VERSION)) { + request->send(304); + return true; + } + return false; +} + +bool setStaticContentCacheHeaders(AsyncWebServerResponse *response) +{ + response->addHeader(F("Cache-Control"),"max-age=2592000"); + response->addHeader(F("ETag"), String(VERSION)); +} void serveIndex(AsyncWebServerRequest* request) { if (handleFileRead(request, "/index.htm")) return; + if (handleIfNoneMatchCacheHeader(request)) return; + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_index, PAGE_index_L); response->addHeader(F("Content-Encoding"),"gzip"); + setStaticContentCacheHeaders(response); request->send(response); }