#pragma once #include "ESPRotary.h" #include #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";