From 245c9a980f4f089af76a70da6ba50cf14c2ade19 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Fri, 23 May 2025 22:18:58 -0500 Subject: [PATCH 01/13] update i2c_encoder usermod to v2 format with json config settings in web ui --- usermods/i2c_encoder_button/README.md | 37 ++++ usermods/i2c_encoder_button/library.json | 9 + .../platformio_override.sample.ini | 19 ++ .../usermod_i2c_encoder_button.cpp | 171 ++++++++++++++++++ wled00/const.h | 1 + 5 files changed, 237 insertions(+) create mode 100644 usermods/i2c_encoder_button/README.md create mode 100644 usermods/i2c_encoder_button/library.json create mode 100644 usermods/i2c_encoder_button/platformio_override.sample.ini create mode 100644 usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp diff --git a/usermods/i2c_encoder_button/README.md b/usermods/i2c_encoder_button/README.md new file mode 100644 index 0000000000..603c1c02d8 --- /dev/null +++ b/usermods/i2c_encoder_button/README.md @@ -0,0 +1,37 @@ +# usermod_i2c_encoder + +This usermod enables the use of a [DUPPA I2CEncoder V2.1](https://www.tindie.com/products/saimon/i2cencoder-v21-connect-rotary-encoder-on-i2c-bus/) rotary encoder + pushbutton to control WLED. + +Settings will be available on the Usermods page of the web UI. Here you can define which pins are used for interrupt, SCL, and SDA. Restart is needed for new values to take effect. + +## Features + +- On/off + - Integrated button switch turns the strip on and off +- Brightness adjust + - Turn the encoder knob to adjust brightness +- Effect adjust (encoder LED turns red) + - Hold the button for 1 second to switch operating mode to effect adjust mode + - When in effect adjust mode the integrated LED turns red + - Rotating the knob cycles through all the effects +- Reset + - When WLED is off (brightness 0) hold the button to reset and load Preset 1. Preset 1 must be defined for this to work. + +## Hardware + +This usermod is intended to work with the I2CEncoder V2.1 with the following configuration: + +- Rotary encoder: Illuminated RGB Encoder + - This encoder includes a pushbutton switch and an internal RGB LED to illuminate the shaft and any know attached to it. + - This is the encoder: https://www.sparkfun.com/products/15141 +- Knob: Any knob works, but the black knob has a transparent ring that lets the internal LED light through for a nice glow. +- Connectors: any +- LEDs: none (this is separate from the LED included in the encoder above) + +## Compiling + +Simply add `custom_usermods = i2c_encoder_button` to your platformio_override.ini environment to enable this usermod in your build. + +See `platformio_override.sample.ini` for example usage. + +Warning: if this usermod is enabled and no i2c encoder is connected you will have problems! diff --git a/usermods/i2c_encoder_button/library.json b/usermods/i2c_encoder_button/library.json new file mode 100644 index 0000000000..08f62b73dd --- /dev/null +++ b/usermods/i2c_encoder_button/library.json @@ -0,0 +1,9 @@ +{ + "name": "i2c_encoder_button", + "version": "1.0.0", + "description": "WLED usermod for DUPPA I2C Encoder rotary encoder.", + "dependencies": { + "Wire": "Wire", + "ArduinoDuPPaLib": "https://github.com/Fattoresaimon/ArduinoDuPPaLib#v1.4.1" + } +} diff --git a/usermods/i2c_encoder_button/platformio_override.sample.ini b/usermods/i2c_encoder_button/platformio_override.sample.ini new file mode 100644 index 0000000000..f4437fd828 --- /dev/null +++ b/usermods/i2c_encoder_button/platformio_override.sample.ini @@ -0,0 +1,19 @@ +; Example platformio_override.ini that shows how to configure your environment to use the I2C Encoder Button usermod. + +[platformio] +default_envs = esp01_i2c_encoder +; default_envs = esp32_i2c_encoder + +; Example using esp01 module with i2c encoder. +; LEDPIN defaults to 2 so it needs to be defined here to avoid conflicts with SCL/SDA pins. +[env:esp01_i2c_encoder] +extends = env:esp01_1m_ota +custom_usermods = ${env:esp01_1m_ota.custom_usermods} i2c_encoder_button +build_flags = ${env:esp01_1m_ota.build_flags} + -D LEDPIN=3 + +; Example for esp32 +; Pins 21 and 22 are default i2c pins on esp32 +[env:esp32_i2c_encoder] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} i2c_encoder_button diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp new file mode 100644 index 0000000000..5112762828 --- /dev/null +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -0,0 +1,171 @@ +#include "wled.h" +#include +#include + +// Default values for I2C encoder pins and address +#ifndef I2C_ENCODER_DEFAULT_ENABLED +#define I2C_ENCODER_DEFAULT_ENABLED false +#endif +#ifndef I2C_ENCODER_DEFAULT_INT_PIN +#define I2C_ENCODER_DEFAULT_INT_PIN 1 +#endif +#ifndef I2C_ENCODER_DEFAULT_SDA_PIN +#define I2C_ENCODER_DEFAULT_SDA_PIN 0 +#endif +#ifndef I2C_ENCODER_DEFAULT_SCL_PIN +#define I2C_ENCODER_DEFAULT_SCL_PIN 2 +#endif +#ifndef I2C_ENCODER_DEFAULT_ADDRESS +#define I2C_ENCODER_DEFAULT_ADDRESS 0x00 +#endif + +// v2 usermod for I2C Encoder +class UsermodI2CEncoderButton : public Usermod { +private: + i2cEncoderLibV2 * encoder_p; + bool encoderButtonDown = false; + uint32_t buttonPressStartTime = 0; // millis when button was pressed + uint32_t buttonPressDuration = 0; + const uint32_t buttonLongPressThreshold = 1000; // duration threshold for long press (millis) + bool wasLongButtonPress = false; + + // encoderMode keeps track of what function the encoder is controlling + // 0 = brightness + // 1 = effect + uint8_t encoderMode = 0; + // encoderModes keeps track of what color the encoder LED should be for each mode + const uint32_t encoderModes[2] = {0x0000FF, 0xFF0000}; + uint32_t lastInteractionTime = 0; + const uint32_t modeResetTimeout = 30000; // timeout for reseting mode to 0 + const uint32_t brightnessDelta = 16; + bool enabled = false; + bool initDone = false; + // Configurable pins and address (now user-configurable via JSON config) + int8_t intPin = I2C_ENCODER_DEFAULT_INT_PIN; // Interrupt pin for I2C encoder + int8_t sdaPin = I2C_ENCODER_DEFAULT_SDA_PIN; // I2C SDA pin + int8_t sclPin = I2C_ENCODER_DEFAULT_SCL_PIN; // I2C SCL pin + uint8_t i2cAddress = I2C_ENCODER_DEFAULT_ADDRESS; // I2C address of encoder + + void updateBrightness(int8_t deltaBrightness) { + bri = constrain(bri + deltaBrightness, 0, 255); + colorUpdated(CALL_MODE_BUTTON); + } + + void updateEffect(int8_t deltaEffect) + { + // set new effect with rollover at 0 and MODE_COUNT + effectCurrent = (effectCurrent + MODE_COUNT + deltaEffect) % MODE_COUNT; + colorUpdated(CALL_MODE_FX_CHANGED); + } + + void setEncoderMode(uint8_t mode) + { + // set new mode and update encoder LED color + encoderMode = mode; + encoder_p->writeRGBCode(encoderModes[encoderMode]); + } + void handleEncoderShortButtonPress() { + toggleOnOff(); + colorUpdated(CALL_MODE_BUTTON); + setEncoderMode(0); + } + void handlEncoderLongButtonPress() { + if (encoderMode == 0 && bri == 0) { + applyPreset(1); + colorUpdated(CALL_MODE_FX_CHANGED); + } else { + setEncoderMode((encoderMode + 1) % (sizeof(encoderModes) / sizeof(encoderModes[0]))); + } + buttonPressStartTime = millis(); + wasLongButtonPress = true; + } + void encoderRotated(i2cEncoderLibV2 *obj) { + switch (encoderMode) { + case 0: updateBrightness(obj->readStatus(i2cEncoderLibV2::RINC) ? brightnessDelta : -brightnessDelta); break; + case 1: updateEffect(obj->readStatus(i2cEncoderLibV2::RINC) ? 1 : -1); break; + } + lastInteractionTime = millis(); + } + void encoderButtonPush(i2cEncoderLibV2 *obj) { + encoderButtonDown = true; + buttonPressStartTime = lastInteractionTime = millis(); + } + void encoderButtonRelease(i2cEncoderLibV2 *obj) { + encoderButtonDown = false; + if (!wasLongButtonPress) handleEncoderShortButtonPress(); + wasLongButtonPress = false; + buttonPressDuration = 0; + lastInteractionTime = millis(); + } +public: + UsermodI2CEncoderButton() { + encoder_p = nullptr; + } + void setup() override { + // (Re)initialize encoder with current config + if (encoder_p) delete encoder_p; + encoder_p = new i2cEncoderLibV2(i2cAddress); + pinMode(intPin, INPUT); + Wire.begin(sdaPin, sclPin); + encoder_p->reset(); + encoder_p->begin( + i2cEncoderLibV2::INT_DATA | i2cEncoderLibV2::WRAP_ENABLE | i2cEncoderLibV2::DIRE_RIGHT | + i2cEncoderLibV2::IPUP_ENABLE | i2cEncoderLibV2::RMOD_X1 | i2cEncoderLibV2::RGB_ENCODER + ); + + encoder_p->writeCounter(0); /* Reset the counter value */ + encoder_p->writeMax(255); /* Set the maximum threshold*/ + encoder_p->writeMin(0); /* Set the minimum threshold */ + encoder_p->writeStep(1); /* Set the step to 1*/ + encoder_p->writeAntibouncingPeriod(5); + encoder_p->writeFadeRGB(1); + encoder_p->writeInterruptConfig( + i2cEncoderLibV2::RINC | i2cEncoderLibV2::RDEC | i2cEncoderLibV2::PUSHP | i2cEncoderLibV2::PUSHR + ); + setEncoderMode(0); + initDone = true; + } + void loop() override { + if (!enabled) return; + if (digitalRead(intPin) == LOW) { + if (encoder_p->updateStatus()) { + if (encoder_p->readStatus(i2cEncoderLibV2::RINC) || encoder_p->readStatus(i2cEncoderLibV2::RDEC)) encoderRotated(encoder_p); + if (encoder_p->readStatus(i2cEncoderLibV2::PUSHP)) encoderButtonPush(encoder_p); + if (encoder_p->readStatus(i2cEncoderLibV2::PUSHR)) encoderButtonRelease(encoder_p); + } + } + if (encoderButtonDown) buttonPressDuration = millis() - buttonPressStartTime; + if (buttonPressDuration > buttonLongPressThreshold) handlEncoderLongButtonPress(); + if (encoderMode != 0 && millis() - lastInteractionTime > modeResetTimeout) setEncoderMode(0); + } + void addToJsonInfo(JsonObject& root) override { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray arr = user.createNestedArray(F("I2C Encoder")); + arr.add(enabled ? F("Enabled") : F("Disabled")); + } + void addToConfig(JsonObject& root) override { + // Add user-configurable pins and address to config + JsonObject top = root.createNestedObject(F("I2C_Encoder_Button")); + top["enabled"] = enabled; + top["intPin"] = intPin; + top["sdaPin"] = sdaPin; + top["sclPin"] = sclPin; + top["i2cAddress"] = i2cAddress; + } + bool readFromConfig(JsonObject& root) override { + // Read user-configurable pins and address from config + JsonObject top = root["I2C_Encoder_Button"]; + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top["enabled"], enabled, I2C_ENCODER_DEFAULT_ENABLED); + configComplete &= getJsonValue(top["intPin"], intPin, I2C_ENCODER_DEFAULT_INT_PIN); + configComplete &= getJsonValue(top["sdaPin"], sdaPin, I2C_ENCODER_DEFAULT_SDA_PIN); + configComplete &= getJsonValue(top["sclPin"], sclPin, I2C_ENCODER_DEFAULT_SCL_PIN); + configComplete &= getJsonValue(top["i2cAddress"], i2cAddress, I2C_ENCODER_DEFAULT_ADDRESS); + return configComplete; + } + uint16_t getId() override { return USERMOD_I2C_ENCODER_BUTTON; } // TODO: assign a unique ID in const.h +}; + +static UsermodI2CEncoderButton usermod_i2c_encoder_button; +REGISTER_USERMOD(usermod_i2c_encoder_button); \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index 2b460f3f18..9de66fcf43 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -210,6 +210,7 @@ #define USERMOD_ID_DEEP_SLEEP 55 //Usermod "usermod_deep_sleep.h" #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" +#define USERMOD_I2C_ENCODER_BUTTON 58 //Usermod "usermod_i2c_encoder_button.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot From fc07abcb92814797733289f23433172e1d835ee8 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Sat, 30 Aug 2025 10:58:07 -0500 Subject: [PATCH 02/13] update i2c encoder usermod --- usermods/i2c_encoder_button/library.json | 3 ++- usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp | 2 +- wled00/const.h | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/usermods/i2c_encoder_button/library.json b/usermods/i2c_encoder_button/library.json index 08f62b73dd..90308e9152 100644 --- a/usermods/i2c_encoder_button/library.json +++ b/usermods/i2c_encoder_button/library.json @@ -5,5 +5,6 @@ "dependencies": { "Wire": "Wire", "ArduinoDuPPaLib": "https://github.com/Fattoresaimon/ArduinoDuPPaLib#v1.4.1" - } + }, + "build": { "libArchive": false } } diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index 5112762828..6e68789fee 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -164,7 +164,7 @@ class UsermodI2CEncoderButton : public Usermod { configComplete &= getJsonValue(top["i2cAddress"], i2cAddress, I2C_ENCODER_DEFAULT_ADDRESS); return configComplete; } - uint16_t getId() override { return USERMOD_I2C_ENCODER_BUTTON; } // TODO: assign a unique ID in const.h + uint16_t getId() override { return USERMOD_I2C_ENCODER_BUTTON; } }; static UsermodI2CEncoderButton usermod_i2c_encoder_button; diff --git a/wled00/const.h b/wled00/const.h index c19843c422..d7749349b2 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -199,6 +199,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" +#define USERMOD_I2C_ENCODER_BUTTON 59 //Usermod "usermod_i2c_encoder_button.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot From 2b23d1cd8119a0737f49cf8ffdb00eb22fb0756f Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 29 Dec 2025 21:02:43 -0600 Subject: [PATCH 03/13] fix usermod id #define --- wled00/const.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/const.h b/wled00/const.h index 38745c065c..4492803dee 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -207,7 +207,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" -#define USERMOD_I2C_ENCODER_BUTTON 59 //Usermod "usermod_i2c_encoder_button.h" +#define USERMOD_ID_I2C_ENCODER_BUTTON 59 //Usermod "usermod_i2c_encoder_button.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot From 5d1a0b8761858d88e224f9a9622c7674db331130 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 29 Dec 2025 21:08:24 -0600 Subject: [PATCH 04/13] update url in readme --- usermods/i2c_encoder_button/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/i2c_encoder_button/README.md b/usermods/i2c_encoder_button/README.md index 603c1c02d8..fd948c06e1 100644 --- a/usermods/i2c_encoder_button/README.md +++ b/usermods/i2c_encoder_button/README.md @@ -1,6 +1,6 @@ # usermod_i2c_encoder -This usermod enables the use of a [DUPPA I2CEncoder V2.1](https://www.tindie.com/products/saimon/i2cencoder-v21-connect-rotary-encoder-on-i2c-bus/) rotary encoder + pushbutton to control WLED. +This usermod enables the use of a [DUPPA I2CEncoder V2.1](https://github.com/DuPPadotnet/I2CEncoderV2.1) rotary encoder + pushbutton to control WLED. Settings will be available on the Usermods page of the web UI. Here you can define which pins are used for interrupt, SCL, and SDA. Restart is needed for new values to take effect. From 0d2ae55093d6ca2c76a3de04b343a14ea627a52a Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 29 Dec 2025 21:13:05 -0600 Subject: [PATCH 05/13] update usermod id comment in const.h --- wled00/const.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/const.h b/wled00/const.h index 4492803dee..bc930abd74 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -207,7 +207,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" -#define USERMOD_ID_I2C_ENCODER_BUTTON 59 //Usermod "usermod_i2c_encoder_button.h" +#define USERMOD_ID_I2C_ENCODER_BUTTON 59 //Usermod "i2c_encoder_button" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot From c109632d6cc6a3ab6b0ef6c9fa6873f7e17576e4 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 5 Jan 2026 09:09:34 -0600 Subject: [PATCH 06/13] fix bugs and implement recommendations from coderabbit --- usermods/i2c_encoder_button/README.md | 4 ++-- .../platformio_override.sample.ini | 5 +++-- .../usermod_i2c_encoder_button.cpp | 16 +++++++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/usermods/i2c_encoder_button/README.md b/usermods/i2c_encoder_button/README.md index fd948c06e1..44a2e51052 100644 --- a/usermods/i2c_encoder_button/README.md +++ b/usermods/i2c_encoder_button/README.md @@ -22,8 +22,8 @@ Settings will be available on the Usermods page of the web UI. Here you can defi This usermod is intended to work with the I2CEncoder V2.1 with the following configuration: - Rotary encoder: Illuminated RGB Encoder - - This encoder includes a pushbutton switch and an internal RGB LED to illuminate the shaft and any know attached to it. - - This is the encoder: https://www.sparkfun.com/products/15141 + - This encoder includes a pushbutton switch and an internal RGB LED to illuminate the shaft and any knob attached to it. + - This is the encoder: [Sparkfun RGB Encoder](https://www.sparkfun.com/products/15141) - Knob: Any knob works, but the black knob has a transparent ring that lets the internal LED light through for a nice glow. - Connectors: any - LEDs: none (this is separate from the LED included in the encoder above) diff --git a/usermods/i2c_encoder_button/platformio_override.sample.ini b/usermods/i2c_encoder_button/platformio_override.sample.ini index f4437fd828..f9f41f0251 100644 --- a/usermods/i2c_encoder_button/platformio_override.sample.ini +++ b/usermods/i2c_encoder_button/platformio_override.sample.ini @@ -1,8 +1,9 @@ ; Example platformio_override.ini that shows how to configure your environment to use the I2C Encoder Button usermod. [platformio] -default_envs = esp01_i2c_encoder -; default_envs = esp32_i2c_encoder +default_envs = + esp01_i2c_encoder + esp32_i2c_encoder ; Example using esp01 module with i2c encoder. ; LEDPIN defaults to 2 so it needs to be defined here to avoid conflicts with SCL/SDA pins. diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index 6e68789fee..89c72f9b44 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -37,7 +37,7 @@ class UsermodI2CEncoderButton : public Usermod { const uint32_t encoderModes[2] = {0x0000FF, 0xFF0000}; uint32_t lastInteractionTime = 0; const uint32_t modeResetTimeout = 30000; // timeout for reseting mode to 0 - const uint32_t brightnessDelta = 16; + const int8_t brightnessDelta = 16; bool enabled = false; bool initDone = false; // Configurable pins and address (now user-configurable via JSON config) @@ -51,15 +51,13 @@ class UsermodI2CEncoderButton : public Usermod { colorUpdated(CALL_MODE_BUTTON); } - void updateEffect(int8_t deltaEffect) - { + void updateEffect(int8_t deltaEffect) { // set new effect with rollover at 0 and MODE_COUNT effectCurrent = (effectCurrent + MODE_COUNT + deltaEffect) % MODE_COUNT; colorUpdated(CALL_MODE_FX_CHANGED); } - void setEncoderMode(uint8_t mode) - { + void setEncoderMode(uint8_t mode) { // set new mode and update encoder LED color encoderMode = mode; encoder_p->writeRGBCode(encoderModes[encoderMode]); @@ -69,7 +67,7 @@ class UsermodI2CEncoderButton : public Usermod { colorUpdated(CALL_MODE_BUTTON); setEncoderMode(0); } - void handlEncoderLongButtonPress() { + void handleEncoderLongButtonPress() { if (encoderMode == 0 && bri == 0) { applyPreset(1); colorUpdated(CALL_MODE_FX_CHANGED); @@ -126,7 +124,7 @@ class UsermodI2CEncoderButton : public Usermod { initDone = true; } void loop() override { - if (!enabled) return; + if (!enabled || !encoder_p) return; if (digitalRead(intPin) == LOW) { if (encoder_p->updateStatus()) { if (encoder_p->readStatus(i2cEncoderLibV2::RINC) || encoder_p->readStatus(i2cEncoderLibV2::RDEC)) encoderRotated(encoder_p); @@ -135,7 +133,7 @@ class UsermodI2CEncoderButton : public Usermod { } } if (encoderButtonDown) buttonPressDuration = millis() - buttonPressStartTime; - if (buttonPressDuration > buttonLongPressThreshold) handlEncoderLongButtonPress(); + if (buttonPressDuration > buttonLongPressThreshold) handleEncoderLongButtonPress(); if (encoderMode != 0 && millis() - lastInteractionTime > modeResetTimeout) setEncoderMode(0); } void addToJsonInfo(JsonObject& root) override { @@ -164,7 +162,7 @@ class UsermodI2CEncoderButton : public Usermod { configComplete &= getJsonValue(top["i2cAddress"], i2cAddress, I2C_ENCODER_DEFAULT_ADDRESS); return configComplete; } - uint16_t getId() override { return USERMOD_I2C_ENCODER_BUTTON; } + uint16_t getId() override { return USERMOD_ID_I2C_ENCODER_BUTTON; } }; static UsermodI2CEncoderButton usermod_i2c_encoder_button; From ff49287a5a141a9d5a6417cf404dc384cb21427f Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 5 Jan 2026 11:15:56 -0600 Subject: [PATCH 07/13] use explicit types for encoder params (fix CI build issue on esp32c3) --- .../i2c_encoder_button/usermod_i2c_encoder_button.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index 89c72f9b44..b1801924c3 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -111,10 +111,10 @@ class UsermodI2CEncoderButton : public Usermod { i2cEncoderLibV2::IPUP_ENABLE | i2cEncoderLibV2::RMOD_X1 | i2cEncoderLibV2::RGB_ENCODER ); - encoder_p->writeCounter(0); /* Reset the counter value */ - encoder_p->writeMax(255); /* Set the maximum threshold*/ - encoder_p->writeMin(0); /* Set the minimum threshold */ - encoder_p->writeStep(1); /* Set the step to 1*/ + encoder_p->writeCounter((int32_t)0); // Reset the counter value + encoder_p->writeMax((int32_t)255); // Set the maximum threshold + encoder_p->writeMin((int32_t)0); // Set the minimum threshold + encoder_p->writeStep((int32_t)1); // Set the step to 1 encoder_p->writeAntibouncingPeriod(5); encoder_p->writeFadeRGB(1); encoder_p->writeInterruptConfig( From 996f12bbd479a11eb0884bebf3f3498d7dd3fd11 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 5 Jan 2026 11:17:04 -0600 Subject: [PATCH 08/13] don't run setup if !enabled --- usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index b1801924c3..0c213be2f8 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -101,7 +101,11 @@ class UsermodI2CEncoderButton : public Usermod { } void setup() override { // (Re)initialize encoder with current config - if (encoder_p) delete encoder_p; + if (encoder_p) { + delete encoder_p; + encoder_p = nullptr; + } + if (!enabled) return; encoder_p = new i2cEncoderLibV2(i2cAddress); pinMode(intPin, INPUT); Wire.begin(sdaPin, sclPin); From 43fe90c7fc5b326c8c5717f85f5735bb48144133 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 5 Jan 2026 11:17:10 -0600 Subject: [PATCH 09/13] formatting --- .../usermod_i2c_encoder_button.cpp | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index 0c213be2f8..2292f81274 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -24,22 +24,23 @@ class UsermodI2CEncoderButton : public Usermod { private: i2cEncoderLibV2 * encoder_p; bool encoderButtonDown = false; - uint32_t buttonPressStartTime = 0; // millis when button was pressed + uint32_t buttonPressStartTime = 0; // Millis when button was pressed uint32_t buttonPressDuration = 0; - const uint32_t buttonLongPressThreshold = 1000; // duration threshold for long press (millis) + const uint32_t buttonLongPressThreshold = 1000; // Duration threshold for long press (millis) bool wasLongButtonPress = false; - // encoderMode keeps track of what function the encoder is controlling + // EncoderMode keeps track of what function the encoder is controlling // 0 = brightness // 1 = effect uint8_t encoderMode = 0; - // encoderModes keeps track of what color the encoder LED should be for each mode + // EncoderModes keeps track of what color the encoder LED should be for each mode const uint32_t encoderModes[2] = {0x0000FF, 0xFF0000}; uint32_t lastInteractionTime = 0; - const uint32_t modeResetTimeout = 30000; // timeout for reseting mode to 0 + const uint32_t modeResetTimeout = 30000; // Timeout for reseting mode to 0 const int8_t brightnessDelta = 16; bool enabled = false; bool initDone = false; + // Configurable pins and address (now user-configurable via JSON config) int8_t intPin = I2C_ENCODER_DEFAULT_INT_PIN; // Interrupt pin for I2C encoder int8_t sdaPin = I2C_ENCODER_DEFAULT_SDA_PIN; // I2C SDA pin @@ -52,21 +53,23 @@ class UsermodI2CEncoderButton : public Usermod { } void updateEffect(int8_t deltaEffect) { - // set new effect with rollover at 0 and MODE_COUNT + // Set new effect with rollover at 0 and MODE_COUNT effectCurrent = (effectCurrent + MODE_COUNT + deltaEffect) % MODE_COUNT; colorUpdated(CALL_MODE_FX_CHANGED); } void setEncoderMode(uint8_t mode) { - // set new mode and update encoder LED color + // Set new mode and update encoder LED color encoderMode = mode; encoder_p->writeRGBCode(encoderModes[encoderMode]); } + void handleEncoderShortButtonPress() { toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); setEncoderMode(0); } + void handleEncoderLongButtonPress() { if (encoderMode == 0 && bri == 0) { applyPreset(1); @@ -77,6 +80,7 @@ class UsermodI2CEncoderButton : public Usermod { buttonPressStartTime = millis(); wasLongButtonPress = true; } + void encoderRotated(i2cEncoderLibV2 *obj) { switch (encoderMode) { case 0: updateBrightness(obj->readStatus(i2cEncoderLibV2::RINC) ? brightnessDelta : -brightnessDelta); break; @@ -84,10 +88,12 @@ class UsermodI2CEncoderButton : public Usermod { } lastInteractionTime = millis(); } + void encoderButtonPush(i2cEncoderLibV2 *obj) { encoderButtonDown = true; buttonPressStartTime = lastInteractionTime = millis(); } + void encoderButtonRelease(i2cEncoderLibV2 *obj) { encoderButtonDown = false; if (!wasLongButtonPress) handleEncoderShortButtonPress(); @@ -95,10 +101,13 @@ class UsermodI2CEncoderButton : public Usermod { buttonPressDuration = 0; lastInteractionTime = millis(); } + public: + UsermodI2CEncoderButton() { encoder_p = nullptr; } + void setup() override { // (Re)initialize encoder with current config if (encoder_p) { @@ -127,6 +136,7 @@ class UsermodI2CEncoderButton : public Usermod { setEncoderMode(0); initDone = true; } + void loop() override { if (!enabled || !encoder_p) return; if (digitalRead(intPin) == LOW) { @@ -140,12 +150,14 @@ class UsermodI2CEncoderButton : public Usermod { if (buttonPressDuration > buttonLongPressThreshold) handleEncoderLongButtonPress(); if (encoderMode != 0 && millis() - lastInteractionTime > modeResetTimeout) setEncoderMode(0); } + void addToJsonInfo(JsonObject& root) override { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray arr = user.createNestedArray(F("I2C Encoder")); arr.add(enabled ? F("Enabled") : F("Disabled")); } + void addToConfig(JsonObject& root) override { // Add user-configurable pins and address to config JsonObject top = root.createNestedObject(F("I2C_Encoder_Button")); @@ -155,6 +167,7 @@ class UsermodI2CEncoderButton : public Usermod { top["sclPin"] = sclPin; top["i2cAddress"] = i2cAddress; } + bool readFromConfig(JsonObject& root) override { // Read user-configurable pins and address from config JsonObject top = root["I2C_Encoder_Button"]; @@ -166,8 +179,9 @@ class UsermodI2CEncoderButton : public Usermod { configComplete &= getJsonValue(top["i2cAddress"], i2cAddress, I2C_ENCODER_DEFAULT_ADDRESS); return configComplete; } + uint16_t getId() override { return USERMOD_ID_I2C_ENCODER_BUTTON; } }; static UsermodI2CEncoderButton usermod_i2c_encoder_button; -REGISTER_USERMOD(usermod_i2c_encoder_button); \ No newline at end of file +REGISTER_USERMOD(usermod_i2c_encoder_button); From 3b831938983d4837db38eca9208863752a129eb7 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Mon, 12 Jan 2026 23:22:56 -0600 Subject: [PATCH 10/13] use global i2c pins and pinmanager for irq --- .../usermod_i2c_encoder_button.cpp | 87 ++++++++++++------- wled00/pin_manager.h | 3 +- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index 2292f81274..034c54679a 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -9,12 +9,12 @@ #ifndef I2C_ENCODER_DEFAULT_INT_PIN #define I2C_ENCODER_DEFAULT_INT_PIN 1 #endif -#ifndef I2C_ENCODER_DEFAULT_SDA_PIN -#define I2C_ENCODER_DEFAULT_SDA_PIN 0 -#endif -#ifndef I2C_ENCODER_DEFAULT_SCL_PIN -#define I2C_ENCODER_DEFAULT_SCL_PIN 2 -#endif +// #ifndef I2C_ENCODER_DEFAULT_SDA_PIN +// #define I2C_ENCODER_DEFAULT_SDA_PIN 0 +// #endif +// #ifndef I2C_ENCODER_DEFAULT_SCL_PIN +// #define I2C_ENCODER_DEFAULT_SCL_PIN 2 +// #endif #ifndef I2C_ENCODER_DEFAULT_ADDRESS #define I2C_ENCODER_DEFAULT_ADDRESS 0x00 #endif @@ -22,6 +22,7 @@ // v2 usermod for I2C Encoder class UsermodI2CEncoderButton : public Usermod { private: + static const char _name[]; i2cEncoderLibV2 * encoder_p; bool encoderButtonDown = false; uint32_t buttonPressStartTime = 0; // Millis when button was pressed @@ -39,23 +40,28 @@ class UsermodI2CEncoderButton : public Usermod { const uint32_t modeResetTimeout = 30000; // Timeout for reseting mode to 0 const int8_t brightnessDelta = 16; bool enabled = false; - bool initDone = false; // Configurable pins and address (now user-configurable via JSON config) - int8_t intPin = I2C_ENCODER_DEFAULT_INT_PIN; // Interrupt pin for I2C encoder - int8_t sdaPin = I2C_ENCODER_DEFAULT_SDA_PIN; // I2C SDA pin - int8_t sclPin = I2C_ENCODER_DEFAULT_SCL_PIN; // I2C SCL pin + int8_t irqPin = I2C_ENCODER_DEFAULT_INT_PIN; // Interrupt pin for I2C encoder + // int8_t sdaPin = I2C_ENCODER_DEFAULT_SDA_PIN; // I2C SDA pin + // int8_t sclPin = I2C_ENCODER_DEFAULT_SCL_PIN; // I2C SCL pin uint8_t i2cAddress = I2C_ENCODER_DEFAULT_ADDRESS; // I2C address of encoder + void update() { + stateUpdated(CALL_MODE_BUTTON); + updateInterfaces(CALL_MODE_BUTTON); + } + void updateBrightness(int8_t deltaBrightness) { bri = constrain(bri + deltaBrightness, 0, 255); - colorUpdated(CALL_MODE_BUTTON); + update(); } void updateEffect(int8_t deltaEffect) { // Set new effect with rollover at 0 and MODE_COUNT effectCurrent = (effectCurrent + MODE_COUNT + deltaEffect) % MODE_COUNT; - colorUpdated(CALL_MODE_FX_CHANGED); + // colorUpdated(CALL_MODE_FX_CHANGED); + update(); } void setEncoderMode(uint8_t mode) { @@ -65,15 +71,18 @@ class UsermodI2CEncoderButton : public Usermod { } void handleEncoderShortButtonPress() { + DEBUG_PRINTLN(F("Encoder short button press")); toggleOnOff(); - colorUpdated(CALL_MODE_BUTTON); + update(); setEncoderMode(0); } void handleEncoderLongButtonPress() { + DEBUG_PRINTLN(F("Encoder long button press")); if (encoderMode == 0 && bri == 0) { applyPreset(1); - colorUpdated(CALL_MODE_FX_CHANGED); + // colorUpdated(CALL_MODE_FX_CHANGED); + update(); } else { setEncoderMode((encoderMode + 1) % (sizeof(encoderModes) / sizeof(encoderModes[0]))); } @@ -82,6 +91,7 @@ class UsermodI2CEncoderButton : public Usermod { } void encoderRotated(i2cEncoderLibV2 *obj) { + DEBUG_PRINTLN(F("Encoder rotated")); switch (encoderMode) { case 0: updateBrightness(obj->readStatus(i2cEncoderLibV2::RINC) ? brightnessDelta : -brightnessDelta); break; case 1: updateEffect(obj->readStatus(i2cEncoderLibV2::RINC) ? 1 : -1); break; @@ -90,11 +100,13 @@ class UsermodI2CEncoderButton : public Usermod { } void encoderButtonPush(i2cEncoderLibV2 *obj) { + DEBUG_PRINTLN(F("Encoder button pushed")); encoderButtonDown = true; buttonPressStartTime = lastInteractionTime = millis(); } void encoderButtonRelease(i2cEncoderLibV2 *obj) { + DEBUG_PRINTLN(F("Encoder button released")); encoderButtonDown = false; if (!wasLongButtonPress) handleEncoderShortButtonPress(); wasLongButtonPress = false; @@ -109,6 +121,22 @@ class UsermodI2CEncoderButton : public Usermod { } void setup() override { + + if (i2c_sda < 0 || i2c_scl < 0) { + DEBUG_PRINTLN(F("I2C pins not set, disabling I2C encoder usermod.")); + enabled = false; + return; + } else { + if (irqPin >= 0 && PinManager::allocatePin(irqPin, false, PinOwner::UM_I2C_ENCODER_BUTTON)) { + pinMode(irqPin, INPUT); + } else { + DEBUG_PRINTLN(F("Unable to allocate interrupt pin, disabling I2C encoder usermod.")); + irqPin = -1; + enabled = false; + return; + } + } + // (Re)initialize encoder with current config if (encoder_p) { delete encoder_p; @@ -116,8 +144,6 @@ class UsermodI2CEncoderButton : public Usermod { } if (!enabled) return; encoder_p = new i2cEncoderLibV2(i2cAddress); - pinMode(intPin, INPUT); - Wire.begin(sdaPin, sclPin); encoder_p->reset(); encoder_p->begin( i2cEncoderLibV2::INT_DATA | i2cEncoderLibV2::WRAP_ENABLE | i2cEncoderLibV2::DIRE_RIGHT | @@ -128,18 +154,17 @@ class UsermodI2CEncoderButton : public Usermod { encoder_p->writeMax((int32_t)255); // Set the maximum threshold encoder_p->writeMin((int32_t)0); // Set the minimum threshold encoder_p->writeStep((int32_t)1); // Set the step to 1 - encoder_p->writeAntibouncingPeriod(5); + encoder_p->writeAntibouncingPeriod(20); encoder_p->writeFadeRGB(1); encoder_p->writeInterruptConfig( i2cEncoderLibV2::RINC | i2cEncoderLibV2::RDEC | i2cEncoderLibV2::PUSHP | i2cEncoderLibV2::PUSHR ); setEncoderMode(0); - initDone = true; } void loop() override { if (!enabled || !encoder_p) return; - if (digitalRead(intPin) == LOW) { + if (digitalRead(irqPin) == LOW) { if (encoder_p->updateStatus()) { if (encoder_p->readStatus(i2cEncoderLibV2::RINC) || encoder_p->readStatus(i2cEncoderLibV2::RDEC)) encoderRotated(encoder_p); if (encoder_p->readStatus(i2cEncoderLibV2::PUSHP)) encoderButtonPush(encoder_p); @@ -160,28 +185,32 @@ class UsermodI2CEncoderButton : public Usermod { void addToConfig(JsonObject& root) override { // Add user-configurable pins and address to config - JsonObject top = root.createNestedObject(F("I2C_Encoder_Button")); + JsonObject top = root.createNestedObject(FPSTR(_name)); top["enabled"] = enabled; - top["intPin"] = intPin; - top["sdaPin"] = sdaPin; - top["sclPin"] = sclPin; - top["i2cAddress"] = i2cAddress; + top["irq_pin"] = irqPin; + // top["sdaPin"] = sdaPin; + // top["sclPin"] = sclPin; + top["i2c_address"] = i2cAddress; + // JsonArray pinArray = top.createNestedArray("pin"); + // pinArray.add(intPin); } bool readFromConfig(JsonObject& root) override { // Read user-configurable pins and address from config - JsonObject top = root["I2C_Encoder_Button"]; + JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["enabled"], enabled, I2C_ENCODER_DEFAULT_ENABLED); - configComplete &= getJsonValue(top["intPin"], intPin, I2C_ENCODER_DEFAULT_INT_PIN); - configComplete &= getJsonValue(top["sdaPin"], sdaPin, I2C_ENCODER_DEFAULT_SDA_PIN); - configComplete &= getJsonValue(top["sclPin"], sclPin, I2C_ENCODER_DEFAULT_SCL_PIN); - configComplete &= getJsonValue(top["i2cAddress"], i2cAddress, I2C_ENCODER_DEFAULT_ADDRESS); + configComplete &= getJsonValue(top["irq_pin"], irqPin, I2C_ENCODER_DEFAULT_INT_PIN); + // configComplete &= getJsonValue(top["sdaPin"], sdaPin, I2C_ENCODER_DEFAULT_SDA_PIN); + // configComplete &= getJsonValue(top["sclPin"], sclPin, I2C_ENCODER_DEFAULT_SCL_PIN); + configComplete &= getJsonValue(top["i2c_address"], i2cAddress, I2C_ENCODER_DEFAULT_ADDRESS); return configComplete; } uint16_t getId() override { return USERMOD_ID_I2C_ENCODER_BUTTON; } }; +const char UsermodI2CEncoderButton::_name[] PROGMEM = "i2c_encoder_button"; + static UsermodI2CEncoderButton usermod_i2c_encoder_button; REGISTER_USERMOD(usermod_i2c_encoder_button); diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index a488d24f70..6899657c22 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -69,7 +69,8 @@ enum struct PinOwner : uint8_t { UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins - UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY, // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_I2C_ENCODER_BUTTON = USERMOD_ID_I2C_ENCODER_BUTTON // 0x3B // Usermod "usermod_i2c_encoder_button.h" -- Uses interrupt pin and "standard" HW_I2C pins }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); From 5282f25396b178f060e6ea089fe96b93dd1c3905 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Thu, 15 Jan 2026 22:03:47 -0600 Subject: [PATCH 11/13] improve brightness and effect set functions, clean up code --- .../usermod_i2c_encoder_button.cpp | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index 034c54679a..399b426100 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -9,12 +9,6 @@ #ifndef I2C_ENCODER_DEFAULT_INT_PIN #define I2C_ENCODER_DEFAULT_INT_PIN 1 #endif -// #ifndef I2C_ENCODER_DEFAULT_SDA_PIN -// #define I2C_ENCODER_DEFAULT_SDA_PIN 0 -// #endif -// #ifndef I2C_ENCODER_DEFAULT_SCL_PIN -// #define I2C_ENCODER_DEFAULT_SCL_PIN 2 -// #endif #ifndef I2C_ENCODER_DEFAULT_ADDRESS #define I2C_ENCODER_DEFAULT_ADDRESS 0x00 #endif @@ -39,12 +33,10 @@ class UsermodI2CEncoderButton : public Usermod { uint32_t lastInteractionTime = 0; const uint32_t modeResetTimeout = 30000; // Timeout for reseting mode to 0 const int8_t brightnessDelta = 16; - bool enabled = false; + bool enabled = I2C_ENCODER_DEFAULT_ENABLED; // Configurable pins and address (now user-configurable via JSON config) int8_t irqPin = I2C_ENCODER_DEFAULT_INT_PIN; // Interrupt pin for I2C encoder - // int8_t sdaPin = I2C_ENCODER_DEFAULT_SDA_PIN; // I2C SDA pin - // int8_t sclPin = I2C_ENCODER_DEFAULT_SCL_PIN; // I2C SCL pin uint8_t i2cAddress = I2C_ENCODER_DEFAULT_ADDRESS; // I2C address of encoder void update() { @@ -52,15 +44,18 @@ class UsermodI2CEncoderButton : public Usermod { updateInterfaces(CALL_MODE_BUTTON); } - void updateBrightness(int8_t deltaBrightness) { - bri = constrain(bri + deltaBrightness, 0, 255); + void updateBrightness(bool increase) { + int8_t delta = bri < 40 ? brightnessDelta / 2 : brightnessDelta; + bri = constrain(bri + (increase ? delta : -delta), 0, 255); update(); } - void updateEffect(int8_t deltaEffect) { + void updateEffect(bool increase) { // Set new effect with rollover at 0 and MODE_COUNT - effectCurrent = (effectCurrent + MODE_COUNT + deltaEffect) % MODE_COUNT; - // colorUpdated(CALL_MODE_FX_CHANGED); + effectCurrent = (effectCurrent + MODE_COUNT + (increase ? 1 : -1)) % MODE_COUNT; + stateChanged = true; + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.setMode(effectCurrent); update(); } @@ -81,7 +76,6 @@ class UsermodI2CEncoderButton : public Usermod { DEBUG_PRINTLN(F("Encoder long button press")); if (encoderMode == 0 && bri == 0) { applyPreset(1); - // colorUpdated(CALL_MODE_FX_CHANGED); update(); } else { setEncoderMode((encoderMode + 1) % (sizeof(encoderModes) / sizeof(encoderModes[0]))); @@ -93,8 +87,8 @@ class UsermodI2CEncoderButton : public Usermod { void encoderRotated(i2cEncoderLibV2 *obj) { DEBUG_PRINTLN(F("Encoder rotated")); switch (encoderMode) { - case 0: updateBrightness(obj->readStatus(i2cEncoderLibV2::RINC) ? brightnessDelta : -brightnessDelta); break; - case 1: updateEffect(obj->readStatus(i2cEncoderLibV2::RINC) ? 1 : -1); break; + case 0: updateBrightness(obj->readStatus(i2cEncoderLibV2::RINC)); break; + case 1: updateEffect(obj->readStatus(i2cEncoderLibV2::RINC)); break; } lastInteractionTime = millis(); } @@ -149,7 +143,6 @@ class UsermodI2CEncoderButton : public Usermod { i2cEncoderLibV2::INT_DATA | i2cEncoderLibV2::WRAP_ENABLE | i2cEncoderLibV2::DIRE_RIGHT | i2cEncoderLibV2::IPUP_ENABLE | i2cEncoderLibV2::RMOD_X1 | i2cEncoderLibV2::RGB_ENCODER ); - encoder_p->writeCounter((int32_t)0); // Reset the counter value encoder_p->writeMax((int32_t)255); // Set the maximum threshold encoder_p->writeMin((int32_t)0); // Set the minimum threshold @@ -188,11 +181,7 @@ class UsermodI2CEncoderButton : public Usermod { JsonObject top = root.createNestedObject(FPSTR(_name)); top["enabled"] = enabled; top["irq_pin"] = irqPin; - // top["sdaPin"] = sdaPin; - // top["sclPin"] = sclPin; top["i2c_address"] = i2cAddress; - // JsonArray pinArray = top.createNestedArray("pin"); - // pinArray.add(intPin); } bool readFromConfig(JsonObject& root) override { @@ -201,8 +190,6 @@ class UsermodI2CEncoderButton : public Usermod { bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["enabled"], enabled, I2C_ENCODER_DEFAULT_ENABLED); configComplete &= getJsonValue(top["irq_pin"], irqPin, I2C_ENCODER_DEFAULT_INT_PIN); - // configComplete &= getJsonValue(top["sdaPin"], sdaPin, I2C_ENCODER_DEFAULT_SDA_PIN); - // configComplete &= getJsonValue(top["sclPin"], sclPin, I2C_ENCODER_DEFAULT_SCL_PIN); configComplete &= getJsonValue(top["i2c_address"], i2cAddress, I2C_ENCODER_DEFAULT_ADDRESS); return configComplete; } From c9a866565b743e9f3c10ce7c5195214df4158668 Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Sun, 18 Jan 2026 09:11:20 -0600 Subject: [PATCH 12/13] deallocate irq pin if usermod not enabled --- .../usermod_i2c_encoder_button.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp index 399b426100..608f0f4395 100644 --- a/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp +++ b/usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp @@ -115,6 +115,16 @@ class UsermodI2CEncoderButton : public Usermod { } void setup() override { + // Clean up existing encoder if any + if (encoder_p) { + delete encoder_p; + encoder_p = nullptr; + } + + if (!enabled) { + if (irqPin >= 0) PinManager::deallocatePin(irqPin, PinOwner::UM_I2C_ENCODER_BUTTON); + return; + } if (i2c_sda < 0 || i2c_scl < 0) { DEBUG_PRINTLN(F("I2C pins not set, disabling I2C encoder usermod.")); @@ -132,11 +142,6 @@ class UsermodI2CEncoderButton : public Usermod { } // (Re)initialize encoder with current config - if (encoder_p) { - delete encoder_p; - encoder_p = nullptr; - } - if (!enabled) return; encoder_p = new i2cEncoderLibV2(i2cAddress); encoder_p->reset(); encoder_p->begin( @@ -166,7 +171,7 @@ class UsermodI2CEncoderButton : public Usermod { } if (encoderButtonDown) buttonPressDuration = millis() - buttonPressStartTime; if (buttonPressDuration > buttonLongPressThreshold) handleEncoderLongButtonPress(); - if (encoderMode != 0 && millis() - lastInteractionTime > modeResetTimeout) setEncoderMode(0); + if ((encoderMode != 0) && ((millis() - lastInteractionTime) > modeResetTimeout)) setEncoderMode(0); } void addToJsonInfo(JsonObject& root) override { From 654e9cf63cf74acf27d694ffe279a7a69468b57a Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Sun, 18 Jan 2026 09:11:51 -0600 Subject: [PATCH 13/13] update sample override and readme --- usermods/i2c_encoder_button/README.md | 2 +- .../i2c_encoder_button/platformio_override.sample.ini | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/usermods/i2c_encoder_button/README.md b/usermods/i2c_encoder_button/README.md index 44a2e51052..08935c9bae 100644 --- a/usermods/i2c_encoder_button/README.md +++ b/usermods/i2c_encoder_button/README.md @@ -2,7 +2,7 @@ This usermod enables the use of a [DUPPA I2CEncoder V2.1](https://github.com/DuPPadotnet/I2CEncoderV2.1) rotary encoder + pushbutton to control WLED. -Settings will be available on the Usermods page of the web UI. Here you can define which pins are used for interrupt, SCL, and SDA. Restart is needed for new values to take effect. +Settings will be available on the Usermods page of the web UI. Here you can define which pins are used for SDA, SCL, and interrupt. Global I2C settings are used for SDA and SCL but interrupt setting will be located in the i2c_encoder_button section at the bottom of the page. Restart is needed for new values to take effect. ## Features diff --git a/usermods/i2c_encoder_button/platformio_override.sample.ini b/usermods/i2c_encoder_button/platformio_override.sample.ini index f9f41f0251..a798f6f265 100644 --- a/usermods/i2c_encoder_button/platformio_override.sample.ini +++ b/usermods/i2c_encoder_button/platformio_override.sample.ini @@ -8,13 +8,15 @@ default_envs = ; Example using esp01 module with i2c encoder. ; LEDPIN defaults to 2 so it needs to be defined here to avoid conflicts with SCL/SDA pins. [env:esp01_i2c_encoder] -extends = env:esp01_1m_ota -custom_usermods = ${env:esp01_1m_ota.custom_usermods} i2c_encoder_button -build_flags = ${env:esp01_1m_ota.build_flags} +extends = env:esp01_1m_full +custom_usermods = ${env:esp01_1m_full.custom_usermods} + i2c_encoder_button +build_flags = ${env:esp01_1m_full.build_flags} -D LEDPIN=3 ; Example for esp32 ; Pins 21 and 22 are default i2c pins on esp32 [env:esp32_i2c_encoder] extends = env:esp32dev -custom_usermods = ${env:esp32dev.custom_usermods} i2c_encoder_button +custom_usermods = ${env:esp32dev.custom_usermods} + i2c_encoder_button