Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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 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

- 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 }
}
22 changes: 22 additions & 0 deletions usermods/i2c_encoder_button/platformio_override.sample.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
; 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_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
208 changes: 208 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,208 @@
#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_ADDRESS
#define I2C_ENCODER_DEFAULT_ADDRESS 0x00
#endif

// 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
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 = 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
uint8_t i2cAddress = I2C_ENCODER_DEFAULT_ADDRESS; // I2C address of encoder

void update() {
stateUpdated(CALL_MODE_BUTTON);
updateInterfaces(CALL_MODE_BUTTON);
}

void updateBrightness(bool increase) {
int8_t delta = bri < 40 ? brightnessDelta / 2 : brightnessDelta;
bri = constrain(bri + (increase ? delta : -delta), 0, 255);
update();
}

void updateEffect(bool increase) {
// Set new effect with rollover at 0 and MODE_COUNT
effectCurrent = (effectCurrent + MODE_COUNT + (increase ? 1 : -1)) % MODE_COUNT;
stateChanged = true;
Segment& seg = strip.getSegment(strip.getMainSegmentId());
seg.setMode(effectCurrent);
update();
}

void setEncoderMode(uint8_t mode) {
// Set new mode and update encoder LED color
encoderMode = mode;
encoder_p->writeRGBCode(encoderModes[encoderMode]);
}

void handleEncoderShortButtonPress() {
DEBUG_PRINTLN(F("Encoder short button press"));
toggleOnOff();
update();
setEncoderMode(0);
}

void handleEncoderLongButtonPress() {
DEBUG_PRINTLN(F("Encoder long button press"));
if (encoderMode == 0 && bri == 0) {
applyPreset(1);
update();
} else {
setEncoderMode((encoderMode + 1) % (sizeof(encoderModes) / sizeof(encoderModes[0])));
}
buttonPressStartTime = millis();
wasLongButtonPress = true;
}

void encoderRotated(i2cEncoderLibV2 *obj) {
DEBUG_PRINTLN(F("Encoder rotated"));
switch (encoderMode) {
case 0: updateBrightness(obj->readStatus(i2cEncoderLibV2::RINC)); break;
case 1: updateEffect(obj->readStatus(i2cEncoderLibV2::RINC)); break;
}
lastInteractionTime = millis();
}

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;
buttonPressDuration = 0;
lastInteractionTime = millis();
}

public:

UsermodI2CEncoderButton() {
encoder_p = nullptr;
}

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."));
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
encoder_p = new i2cEncoderLibV2(i2cAddress);
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(20);
encoder_p->writeFadeRGB(1);
encoder_p->writeInterruptConfig(
i2cEncoderLibV2::RINC | i2cEncoderLibV2::RDEC | i2cEncoderLibV2::PUSHP | i2cEncoderLibV2::PUSHR
);
setEncoderMode(0);
}

void loop() override {
if (!enabled || !encoder_p) return;
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);
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(FPSTR(_name));
top["enabled"] = enabled;
top["irq_pin"] = irqPin;
top["i2c_address"] = i2cAddress;
}

bool readFromConfig(JsonObject& root) override {
// Read user-configurable pins and address from config
JsonObject top = root[FPSTR(_name)];
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["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);
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
3 changes: 2 additions & 1 deletion wled00/pin_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected");

Expand Down
Loading