Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions usermods/i2c_encoder_button/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# usermod_i2c_encoder

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.

## 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 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)

## 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!
10 changes: 10 additions & 0 deletions usermods/i2c_encoder_button/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"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"
},
"build": { "libArchive": false }
}
20 changes: 20 additions & 0 deletions usermods/i2c_encoder_button/platformio_override.sample.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
; Example platformio_override.ini that shows how to configure your environment to use the I2C Encoder Button usermod.

[platformio]
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.
[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
187 changes: 187 additions & 0 deletions usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#include "wled.h"
#include <Wire.h>
#include <i2cEncoderLibV2.h>

// 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 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
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 handleEncoderLongButtonPress() {
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 = nullptr;
}
if (!enabled) return;
encoder_p = new i2cEncoderLibV2(i2cAddress);
pinMode(intPin, INPUT);
Wire.begin(sdaPin, sclPin);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Wire.begin usage patterns in WLED usermods
rg -n -C 3 'Wire\.begin' --type cpp --type h

Repository: wled/WLED

Length of output: 9935


🏁 Script executed:

# Get the specific file and its context around line 107
cat -n usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp | sed -n '95,120p'

Repository: wled/WLED

Length of output: 1282


🏁 Script executed:

# Check for global Wire initialization in main files
rg -n 'Wire\.begin' --type cpp --type h | grep -v 'usermods' | head -20

Repository: wled/WLED

Length of output: 981


Avoid re-initializing Wire in usermod setup().

Calling Wire.begin(sdaPin, sclPin) is redundant; WLED initializes Wire globally once in wled00/cfg.cpp. Re-initialization conflicts with the documented WLED architecture (see wled00/set.cpp line 671: "no bus re-initialisation as usermods do not get any notification"). Additionally, cfg.cpp line 475 contains an explicit warning that Wire.setPins() fails if Wire is already initialized. Remove this call and rely on the global Wire initialization instead.

🤖 Prompt for AI Agents
In @usermods/i2c_encoder_button/usermod_i2c_encoder_button.cpp around line 107,
Remove the redundant Wire.begin(sdaPin, sclPin) call from the usermod setup()
(the re-initialization of the global Wire bus); rely on WLED's global Wire
initialization instead and delete that single Wire.begin(...) line so you don't
conflict with cfg.cpp/set.cpp warnings about bus re-initialisation and
Wire.setPins() failing when already initialized.

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((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(
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 (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) 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"));
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_ID_I2C_ENCODER_BUTTON; }
};

static UsermodI2CEncoderButton usermod_i2c_encoder_button;
REGISTER_USERMOD(usermod_i2c_encoder_button);
1 change: 1 addition & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +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 "i2c_encoder_button"

//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
Expand Down