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..b1e55202 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -0,0 +1,918 @@ +#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_2x + Copyright: + Glyphs: 1/1 + BBX Build Mode: 3 + * 2x2 custom icons that are not available in the U8X8 library + * 64 = custom palette +*/ +const uint8_t u8x8_font_benji_custom_icons_2x2[37] U8X8_FONT_SECTION("u8x8_font_benji_custom_icons_2x2") = + "@@\2\2\360\370\234\236\376\363\363\377\377\363\363\376><\370\360\3\17\77yy\377\377\377\377\317\17\17" + "\17\17\7\3"; + +/* + Fontname: benji_custom_icons_6x + Copyright: + Glyphs: 8/8 + BBX Build Mode: 3 + * 6x6 icons take up a lot of memory, theres not enough momory for the required librries + * these are just the ruquired icons stripped for 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(){ + drawGlyph(1, 0, 69, u8x8_font_open_iconic_weather_2x2, true);//brightness icon + drawGlyph(5, 0, 72, u8x8_font_open_iconic_play_2x2, true);//speed icon + drawGlyph(9, 0, 78, u8x8_font_open_iconic_thing_2x2, true);//intensity icon + drawGlyph(14, 2*lineHeight, 64, u8x8_font_benji_custom_icons_2x2,true);//palette icon + drawGlyph(14, 3*lineHeight, 70, u8x8_font_open_iconic_thing_2x2,true);//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]; + uint8_t qComma = 0; + bool insideQuotes = false; + bool spaceHit = false; + uint8_t printedChars = 0; + uint8_t smallChars1 = 0; + uint8_t smallChars2 = 0; + uint8_t totalCount = 0; + char singleJsonSymbol; + + // Find the mode name in JSON + for (size_t i = 0; i < strlen_P(qstring); i++) { + singleJsonSymbol = pgm_read_byte_near(qstring + i); + if (singleJsonSymbol == '\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(printedChars < (MAX_MODE_LINE_SPACE)){ + for (;printedChars < (MAX_MODE_LINE_SPACE-1); printedChars++) {lineBuffer[printedChars]=' '; } + lineBuffer[printedChars] = 0; + drawString(1, row*lineHeight, lineBuffer); + lastRedraw = millis(); + }else{ + 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(); + } + } + } + + /** + * 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 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, long showHowLong, byte glyphType) { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + } + + // Print the overlay + clear(); + if (glyphType > 0)drawGlyph(5, 0, glyphType, u8x8_font_benji_custom_icons_6x6, 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/usermods_list.cpp b/wled00/usermods_list.cpp index fd5ffffd..475f3434 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -44,10 +44,18 @@ #include "../usermods/BME280_v2/usermod_bme280.h" #endif #ifdef USERMOD_FOUR_LINE_DISPLAY -#include "../usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h" + #ifdef USE_ALT_DISPlAY + #include "../usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h" + #else + #include "../usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h" + #endif #endif #ifdef USERMOD_ROTARY_ENCODER_UI -#include "../usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h" + #ifdef USE_ALT_DISPlAY + #include "../usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h" + #else + #include "../usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h" + #endif #endif #ifdef USERMOD_AUTO_SAVE #include "../usermods/usermod_v2_auto_save/usermod_v2_auto_save.h"