343 lines
11 KiB
C++
343 lines
11 KiB
C++
#pragma once
|
|
|
|
#include "ESPRotary.h"
|
|
#include <math.h>
|
|
#include "wled.h"
|
|
|
|
class RgbRotaryEncoderUsermod : public Usermod
|
|
{
|
|
private:
|
|
bool enabled = false;
|
|
bool initDone = false;
|
|
bool isDirty = false;
|
|
BusDigital *ledBus;
|
|
/*
|
|
* Green - eb - Q4 - 32
|
|
* Red - ea - Q1 - 15
|
|
* Black - sw - Q2 - 12
|
|
*/
|
|
ESPRotary *rotaryEncoder;
|
|
int8_t ledIo = 3; // GPIO to control the LEDs
|
|
int8_t eaIo = 15; // "ea" from RGB Encoder Board
|
|
int8_t ebIo = 32; // "eb" from RGB Encoder Board
|
|
byte stepsPerClick = 4; // How many "steps" your rotary encoder does per click. This varies per rotary encoder
|
|
/* This could vary per rotary encoder: Usually rotary encoders have 20 "clicks".
|
|
If yours has less/more, adjust this to: 100% = 20 LEDs * incrementPerClick */
|
|
byte incrementPerClick = 5;
|
|
byte ledMode = 3;
|
|
byte ledBrightness = 64;
|
|
|
|
// This is all needed to calculate the brightness, rotary position, etc.
|
|
const byte minPos = 5; // minPos is not zero, because if we want to turn the LEDs off, we use the built-in button ;)
|
|
const byte maxPos = 100; // maxPos=100, like 100%
|
|
const byte numLeds = 20;
|
|
byte lastKnownPos = 0;
|
|
|
|
byte currentColors[3];
|
|
byte lastKnownBri = 0;
|
|
|
|
|
|
void initRotaryEncoder()
|
|
{
|
|
PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } };
|
|
if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) {
|
|
eaIo = -1;
|
|
ebIo = -1;
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
// I don't know why, but setting the upper bound here does not work. It results into 1717922932 O_o
|
|
rotaryEncoder = new ESPRotary(eaIo, ebIo, stepsPerClick, incrementPerClick, maxPos, currentPos, incrementPerClick);
|
|
rotaryEncoder->setUpperBound(maxPos); // I have to again set it here and then it works / is actually 100...
|
|
|
|
rotaryEncoder->setChangedHandler(RgbRotaryEncoderUsermod::cbRotate);
|
|
}
|
|
|
|
void initLedBus()
|
|
{
|
|
byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255};
|
|
BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);
|
|
|
|
ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1);
|
|
if (!ledBus->isOk()) {
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
ledBus->setBrightness(ledBrightness);
|
|
}
|
|
|
|
void updateLeds()
|
|
{
|
|
switch (ledMode) {
|
|
case 2:
|
|
{
|
|
currentColors[0] = 255; currentColors[1] = 0; currentColors[2] = 0;
|
|
for (int i = 0; i < currentPos / incrementPerClick - 1; i++) {
|
|
ledBus->setPixelColor(i, 0);
|
|
}
|
|
ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors));
|
|
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
|
|
ledBus->setPixelColor(i, 0);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
case 1:
|
|
case 3:
|
|
// WLED orange (of course), which we will use in mode 1
|
|
currentColors[0] = 255; currentColors[1] = 160; currentColors[2] = 0;
|
|
for (int i = 0; i < currentPos / incrementPerClick; i++) {
|
|
if (ledMode == 3) {
|
|
hsv2rgb((i) / float(numLeds), 1, .25);
|
|
}
|
|
ledBus->setPixelColor(i, colorFromRgbw(currentColors));
|
|
}
|
|
for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
|
|
ledBus->setPixelColor(i, 0);
|
|
}
|
|
break;
|
|
}
|
|
|
|
isDirty = true;
|
|
}
|
|
|
|
void cleanup()
|
|
{
|
|
// Only deallocate pins if we allocated them ;)
|
|
if (eaIo != -1) {
|
|
pinManager.deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder);
|
|
eaIo = -1;
|
|
}
|
|
if (ebIo != -1) {
|
|
pinManager.deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder);
|
|
ebIo = -1;
|
|
}
|
|
|
|
delete rotaryEncoder;
|
|
delete ledBus;
|
|
|
|
enabled = false;
|
|
}
|
|
|
|
int getPositionForBrightness()
|
|
{
|
|
return int(((float)bri / (float)255) * 100);
|
|
}
|
|
|
|
float fract(float x) { return x - int(x); }
|
|
|
|
float mix(float a, float b, float t) { return a + (b - a) * t; }
|
|
|
|
void hsv2rgb(float h, float s, float v) {
|
|
currentColors[0] = int((v * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);
|
|
currentColors[1] = int((v * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);
|
|
currentColors[2] = int((v * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);
|
|
}
|
|
|
|
public:
|
|
static byte currentPos;
|
|
|
|
// strings to reduce flash memory usage (used more than twice)
|
|
static const char _name[];
|
|
static const char _enabled[];
|
|
static const char _ledIo[];
|
|
static const char _eaIo[];
|
|
static const char _ebIo[];
|
|
static const char _ledMode[];
|
|
static const char _ledBrightness[];
|
|
static const char _stepsPerClick[];
|
|
static const char _incrementPerClick[];
|
|
|
|
|
|
static void cbRotate(ESPRotary& r) {
|
|
currentPos = r.getPosition();
|
|
}
|
|
|
|
/**
|
|
* Enable/Disable the usermod
|
|
*/
|
|
// inline void enable(bool enable) { enabled = enable; }
|
|
/**
|
|
* Get usermod enabled/disabled state
|
|
*/
|
|
// inline bool isEnabled() { return enabled; }
|
|
|
|
/*
|
|
* setup() is called once at boot. WiFi is not yet connected at this point.
|
|
* You can use it to initialize variables, sensors or similar.
|
|
*/
|
|
void setup()
|
|
{
|
|
if (enabled) {
|
|
currentPos = getPositionForBrightness();
|
|
lastKnownBri = bri;
|
|
|
|
initRotaryEncoder();
|
|
initLedBus();
|
|
|
|
// No updating of LEDs here, as that's sometimes not working; loop() will take care of that
|
|
|
|
initDone = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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()
|
|
{
|
|
if (!enabled || strip.isUpdating()) return;
|
|
|
|
rotaryEncoder->loop();
|
|
|
|
// If the rotary was changed
|
|
if(lastKnownPos != currentPos) {
|
|
lastKnownPos = currentPos;
|
|
|
|
bri = min(int(round((2.55 * currentPos))), 255);
|
|
lastKnownBri = bri;
|
|
|
|
updateLeds();
|
|
colorUpdated(CALL_MODE_DIRECT_CHANGE);
|
|
}
|
|
|
|
// If the brightness is changed not with the rotary, update the rotary
|
|
if (bri != lastKnownBri) {
|
|
currentPos = lastKnownPos = getPositionForBrightness();
|
|
lastKnownBri = bri;
|
|
rotaryEncoder->resetPosition(currentPos);
|
|
updateLeds();
|
|
}
|
|
|
|
// Update LEDs here in loop to also validate that we can update/show
|
|
if (isDirty && ledBus->canShow()) {
|
|
isDirty = false;
|
|
ledBus->show();
|
|
}
|
|
}
|
|
|
|
void addToConfig(JsonObject &root)
|
|
{
|
|
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
|
|
|
|
top[FPSTR(_enabled)] = enabled;
|
|
top[FPSTR(_ledIo)] = ledIo;
|
|
top[FPSTR(_eaIo)] = eaIo;
|
|
top[FPSTR(_ebIo)] = ebIo;
|
|
top[FPSTR(_ledMode)] = ledMode;
|
|
top[FPSTR(_ledBrightness)] = ledBrightness;
|
|
top[FPSTR(_stepsPerClick)] = stepsPerClick;
|
|
top[FPSTR(_incrementPerClick)] = incrementPerClick;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
JsonObject top = root[FPSTR(_name)];
|
|
if (top.isNull()) {
|
|
DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name);
|
|
return false;
|
|
}
|
|
|
|
bool oldEnabled = enabled;
|
|
int8_t oldLedIo = ledIo;
|
|
int8_t oldEaIo = eaIo;
|
|
int8_t oldEbIo = ebIo;
|
|
byte oldLedMode = ledMode;
|
|
byte oldStepsPerClick = stepsPerClick;
|
|
byte oldIncrementPerClick = incrementPerClick;
|
|
byte oldLedBrightness = ledBrightness;
|
|
|
|
getJsonValue(top[FPSTR(_enabled)], enabled);
|
|
getJsonValue(top[FPSTR(_ledIo)], ledIo);
|
|
getJsonValue(top[FPSTR(_eaIo)], eaIo);
|
|
getJsonValue(top[FPSTR(_ebIo)], ebIo);
|
|
getJsonValue(top[FPSTR(_stepsPerClick)], stepsPerClick);
|
|
getJsonValue(top[FPSTR(_incrementPerClick)], incrementPerClick);
|
|
ledMode = top[FPSTR(_ledMode)] > 0 && top[FPSTR(_ledMode)] < 4 ? top[FPSTR(_ledMode)] : ledMode;
|
|
ledBrightness = top[FPSTR(_ledBrightness)] > 0 && top[FPSTR(_ledBrightness)] <= 255 ? top[FPSTR(_ledBrightness)] : ledBrightness;
|
|
|
|
if (!initDone) {
|
|
// First run: reading from cfg.json
|
|
// Nothing to do here, will be all done in setup()
|
|
}
|
|
// Mod was disabled, so run setup()
|
|
else if (enabled && enabled != oldEnabled) {
|
|
DEBUG_PRINTF("[%s] Usermod has been re-enabled\n", _name);
|
|
setup();
|
|
}
|
|
// Config has been changed, so adopt to changes
|
|
else {
|
|
if (!enabled) {
|
|
DEBUG_PRINTF("[%s] Usermod has been disabled\n", _name);
|
|
cleanup();
|
|
}
|
|
else {
|
|
DEBUG_PRINTF("[%s] Usermod is enabled\n", _name);
|
|
if (ledIo != oldLedIo) {
|
|
delete ledBus;
|
|
initLedBus();
|
|
}
|
|
|
|
if (ledBrightness != oldLedBrightness) {
|
|
ledBus->setBrightness(ledBrightness);
|
|
isDirty = true;
|
|
}
|
|
|
|
if (ledMode != oldLedMode) {
|
|
updateLeds();
|
|
}
|
|
|
|
if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) {
|
|
pinManager.deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder);
|
|
pinManager.deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder);
|
|
|
|
delete rotaryEncoder;
|
|
initRotaryEncoder();
|
|
}
|
|
}
|
|
|
|
DEBUG_PRINTF("[%s] Config (re)loaded\n", _name);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* 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_RGB_ROTARY_ENCODER;
|
|
}
|
|
|
|
//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!
|
|
};
|
|
|
|
byte RgbRotaryEncoderUsermod::currentPos = 5;
|
|
// strings to reduce flash memory usage (used more than twice)
|
|
const char RgbRotaryEncoderUsermod::_name[] PROGMEM = "RGB-Rotary-Encoder";
|
|
const char RgbRotaryEncoderUsermod::_enabled[] PROGMEM = "Enabled";
|
|
const char RgbRotaryEncoderUsermod::_ledIo[] PROGMEM = "LED-pin";
|
|
const char RgbRotaryEncoderUsermod::_eaIo[] PROGMEM = "ea-pin";
|
|
const char RgbRotaryEncoderUsermod::_ebIo[] PROGMEM = "eb-pin";
|
|
const char RgbRotaryEncoderUsermod::_ledMode[] PROGMEM = "LED-Mode";
|
|
const char RgbRotaryEncoderUsermod::_ledBrightness[] PROGMEM = "LED-Brightness";
|
|
const char RgbRotaryEncoderUsermod::_stepsPerClick[] PROGMEM = "Steps-per-Click";
|
|
const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click"; |