Rotary Encoder, Four Line Display, and Auto Save Usermods (#1722)

* Ability to lookup Usermod by id so Usermods can use other Usermods.

* Rotary Encoder UI using two Usermods

* Updates. More to come, probably.

* Updated rotary usermod to honor USE_FOUR_LINE_DISPLAY if you want to use four line display. It should be truly optional, now.

* minor logic improvement to showing the current time in clock mode.

* improved 24 hour display foratting and ability to use the FourLineDisplayUsermod without the RotaryEncoderUIUsermod (option disable sleep and clock modes).

* Improved ordering of defines in the FourLineDisplayUsermod to put options people might need to change together toward the top.

* relocate plugins. add mention of the Wire requirement.

* usermod filenames changed, updating comment in const.h

* fix usermod locations.

* fix usermods_list to include changed folder.

* Improved for both usermods: install, config, and docs. Included sample platform_override.ini.

* Updated name of SDA and SCL defines for config of display

* update docs.

* Wrong year. Fixed.

* Fix youtube link, improve config of sleep/clock when the rotary usermod isn't installed.

* Minor fixes to four line display. Addition of Auto Save v2 usermod.

* Allow config for auto-save to set the preset number to use. Load preset at startup (so brightness is set correctly).

* Updated docs for Auto Save.

* Updated docs for Auto Save.

Co-authored-by: Kevin Dorff <kevin@macbookpro-kevin-wifi.local>
This commit is contained in:
Kevin Dorff 2021-02-09 10:15:43 -06:00 committed by GitHub
parent 94941a7732
commit 8e71c3ae17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1341 additions and 0 deletions

View File

@ -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

View File

@ -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;
}
};

View File

@ -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

View File

@ -0,0 +1,530 @@
#pragma once
#include "wled.h"
#include <U8x8lib.h> // 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;
}
};

View File

@ -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

View File

@ -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

View File

@ -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!
};

View File

@ -21,6 +21,9 @@
#define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h"
#define USERMOD_ID_PIRSWITCH 5 //Usermod "usermod_PIR_sensor_switch.h"
#define USERMOD_ID_IMU 6 //Usermod "usermod_mpu6050_imu.h"
#define USERMOD_ID_FOUR_LINE_DISP 7 //Usermod "usermod_v2_four_line_display.h
#define USERMOD_ID_ROTARY_ENC_UI 8 //Usermod "usermod_v2_rotary_encoder_ui.h"
#define USERMOD_ID_AUTO_SAVE 9 //Usermod "usermod_v2_auto_save.h"
//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot

View File

@ -221,6 +221,7 @@ class UsermodManager {
void readFromConfig(JsonObject& obj);
bool add(Usermod* um);
Usermod* lookup(uint16_t mod_id);
byte getModCount();
};

View File

@ -15,6 +15,18 @@ void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < n
void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(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;

View File

@ -21,6 +21,16 @@
#include "usermod_v2_SensorsToMqtt.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
void registerUsermods()
{
/*
@ -39,4 +49,14 @@ void registerUsermods()
#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
}