Skip to content

Commit

Permalink
Perceptual brightness usermod
Browse files Browse the repository at this point in the history
This usermod adds API commands for changing the global brightness in
steps that look even to the human eye, but in reality are small steps at
the low end and large steps at the high end. There are commands included
to put brightness controls on a single button, ramping the brightness
while the button is held down.
  • Loading branch information
obar committed Dec 5, 2024
1 parent e8d9891 commit 69bbbab
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 0 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
183 changes: 183 additions & 0 deletions usermods/perceptual_brightness/perceptual_brightness.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#pragma once

#include "wled.h"

#ifndef USERMOD_PERCEPTUAL_BRIGHTNESS_MAX_STEPS
#define USERMOD_PERCEPTUAL_BRIGHTNESS_MAX_STEPS 32
#endif

#ifndef USERMOD_PERCEPTUAL_BRIGHTNESS_TOGGLE_DIRECTION_DELAY
#define USERMOD_PERCEPTUAL_BRIGHTNESS_TOGGLE_DIRECTION_DELAY (WLED_LONG_REPEATED_ACTION + 100)
#endif

class UsermodPerceptualBrightness : public Usermod {
private:
bool enabled = false;
bool increasing = false; // used for wrapping brightness control
uint8_t brightness[USERMOD_PERCEPTUAL_BRIGHTNESS_MAX_STEPS];
uint8_t maxBrightness, computeSteps, skipStart, skipEnd;
static const float curveFactor;
uint32_t lastCalled, delta;
static const char *sName, *sEnabled, *sMaxBrightness, *sComputeSteps, *sSkipStart, *sSkipEnd;

int b = 0; // b is current brightness step
int c = 0; // c is last brightness step

inline void computeLevels();
inline void applyBrightness() {
bri = brightness[b];
}

public:
inline void incBrightness();
inline void decBrightness();
inline void cycleBrightness(bool wrap=true);

inline void enable(bool enable) { enabled = enable; }
inline bool isEnabled() { return enabled; }

void setup() override {
return;
}

void loop() override {
return;
}

void readFromJsonState(JsonObject& root) override {
String cmd;
bool cmdExists = getJsonValue(root["pb"], cmd);
if (enabled && cmdExists) {
if (cmd.length() == 1) {
char direction = cmd[0];
switch (direction) {
case '+': incBrightness(); break;
case '-': decBrightness(); break;
case '~': cycleBrightness(); break;
case '/': cycleBrightness(false); break;
default: DEBUG_PRINTLN(F("Perceptual brightness error: unrecognized command"));
}
} else {
DEBUG_PRINTLN(F("Perceptual brightness error: unrecognized command"));
}
}
}

void addToConfig(JsonObject& root) override {
JsonObject top = root.createNestedObject(FPSTR(sName));
top[FPSTR(sEnabled)] = enabled;
top[FPSTR(sMaxBrightness)] = maxBrightness;
top[FPSTR(sComputeSteps)] = computeSteps;
top[FPSTR(sSkipStart)] = skipStart;
top[FPSTR(sSkipEnd)] = skipEnd;
}

void appendConfigData() override {
oappend(F("addInfo('")); oappend(String(FPSTR(sName)).c_str()); oappend(':'); oappend(FPSTR(sMaxBrightness));
oappend(F("',1,'<em>Max: 255</em>');"));

oappend(F("addInfo('")); oappend(String(FPSTR(sName)).c_str()); oappend(':'); oappend(FPSTR(sComputeSteps));
oappend(F("',1,'<em>Max: "));
oappend(USERMOD_PERCEPTUAL_BRIGHTNESS_MAX_STEPS);
oappend(F("<br/>You may want to compute more brightness steps than you will actually use,<br/>eg skipping a few low-brightness steps at the start.<br/>You can skip any number of steps at the beginning and end of the set.');"));

String s = "',1,'<p>All computed steps:<br/>";
for (int i = 0; i < computeSteps; i++) {
if (i == skipStart) s += "[";
s += String(brightness[i]);
if (i == computeSteps - skipEnd - 1) s += "]";
if (i < computeSteps - 1) s +=", ";
}
s += "<br>Only steps in [square brackets] will be used, others skipped</p>');";

oappend(F("addInfo('")); oappend(String(FPSTR(sName)).c_str()); oappend(':'); oappend(FPSTR(sSkipEnd)); oappend(s.c_str());
}

bool readFromConfig(JsonObject& root) override {
JsonObject top = root[FPSTR(sName)];
bool configComplete = !top.isNull();

configComplete &= getJsonValue(top[FPSTR(sEnabled)], enabled, false);
configComplete &= getJsonValue(top[FPSTR(sMaxBrightness)], maxBrightness, 255);
configComplete &= getJsonValue(top[FPSTR(sComputeSteps)], computeSteps, 16);
configComplete &= getJsonValue(top[FPSTR(sSkipStart)], skipStart, 3);
configComplete &= getJsonValue(top[FPSTR(sSkipEnd)], skipEnd, 0);

// Enforce bounds of those values
if (configComplete) {
// pattern is
// \1 = (\1 > \2) ? \2 : \1;
maxBrightness = (maxBrightness > 255) ? 255 : maxBrightness;
computeSteps = (computeSteps > USERMOD_PERCEPTUAL_BRIGHTNESS_MAX_STEPS) ? USERMOD_PERCEPTUAL_BRIGHTNESS_MAX_STEPS : computeSteps;
skipStart = (skipStart > (computeSteps - 1)) ? computeSteps - 1 : skipStart;
skipEnd = (skipEnd > (computeSteps - skipStart - 1)) ? (computeSteps - skipStart - 1) : skipEnd;
}

computeLevels();
return configComplete;
}

};

const float UsermodPerceptualBrightness::curveFactor = 1.0f / 0.33f;
const char* UsermodPerceptualBrightness::sName = "Perceptual Brightness";
const char* UsermodPerceptualBrightness::sEnabled = "Enabled";
const char* UsermodPerceptualBrightness::sMaxBrightness = "Max brightness";
const char* UsermodPerceptualBrightness::sComputeSteps = "Steps to compute";
const char* UsermodPerceptualBrightness::sSkipStart = "Start skip";
const char* UsermodPerceptualBrightness::sSkipEnd = "End skip";

inline void UsermodPerceptualBrightness::computeLevels(){
bool exactMatch = false;
for (int i = 0; i < computeSteps; i++) {
brightness[i] = (uint8_t)(0.5 + pow((1.0 + i)/computeSteps,curveFactor) * maxBrightness);
if (!exactMatch && brightness[i] == bri) {
exactMatch = true;
b = i;
}
}
if (!exactMatch) {
// Current brightness is between steps. Set current step to the first one below the current brightness
// Will still be 0 as initialized if this search finds nothing, ie bri not less than any step.
for (int i = computeSteps - 1; i >= 0; i++) {
if (bri < brightness[i]) {
b = i;
break;
}
}
}
}

void UsermodPerceptualBrightness::incBrightness(){
if (b >= (computeSteps - skipEnd - 1)) b = computeSteps - skipEnd - 1;
else b++;
applyBrightness();
}

void UsermodPerceptualBrightness::decBrightness(){
if (b <= skipStart) b = skipStart;
else b--;
applyBrightness();
}

void UsermodPerceptualBrightness::cycleBrightness(bool wrap){
uint32_t nowTime = millis();
uint32_t interval = nowTime - lastCalled;
lastCalled = nowTime;

if (interval > USERMOD_PERCEPTUAL_BRIGHTNESS_TOGGLE_DIRECTION_DELAY) increasing = !increasing;

if (increasing) {
if (b >= (computeSteps - skipEnd - 1)) {
if (wrap) increasing = false;
b = computeSteps - skipEnd - 1;
} else b++;
} else {
// decreasing brightnesso
if (b <= skipStart) {
if (wrap) increasing = true;
b = skipStart;
} else b--;
}
applyBrightness();
}
42 changes: 42 additions & 0 deletions usermods/perceptual_brightness/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Perceptual Brightness usermod

This usermod lets you make changes to the brightness in 'steps' that are evenly spaced to the human eye.

Our eyes are logarithmic, meaning that the same absolute change when the intensity is low (eg, changing from brightness setting 10 to 20) seems much higher to us than when the intensity is high (eg, changing from 210 to 220). For brightness to change in perceptually even steps, the steps must be small at the bottom end of the brightness range and become larger.

It's easy to apply this usermod for a single button control of global WLED brightness, or for up/down buttons. It can also be used with external calls from Home Assistant or other API-based interfaces.

## Brightness steps
Once you enable the usermod (be sure to check the 'enable' button), you can choose how many brightness steps should be computed, as well as how many steps to skip at the start and end.

![A screenshot of the Perceptual Brightness configuration page, showing 16 steps of brightness levels. They have smaller numerical spacing at the start, going up by only 2 and larger at the end, going up by 45. The screenshot also demonstrates skipping the first 3 and last 2 brightness steps, indicated with square brackets.](perceptual-brightness-example-levels.png)

Note the computed steps, listed in the bottom of the example above. The spacing increases gradually as the brightness goes up. In practice, cycling through these levels looks about linear to the eye.

Also note the start skip of 3 means the first 3 values are outside the square brackets: they will be ignored when using this usermod. The end skip of 2 is also demonstrated, limiting the maximum brightness to 170, but you could also achieve this by setting the max brightness at the top to 170 and computing fewer steps.

This usermod allows for multiple ways to move up or down along these brightness steps, described below.

## Up and down buttons
Sending an API command of `{"pb":"+"}` will move from the current brightness step to the next higher brightness step. In the example above, if WLED's global brightness is at brightness of 21, it will increase to 31.

Lowering the brightness uses the API command `{"pb":"-"}`

You can create presets with these two API commands (uncheck the "use current state" when saving a new preset to enter an API command) and test them by clicking on those presets in the web UI. I use the names `_bri_up` and `_bri_down` but you can call them whatever you'd like.

![A screenshot of a preset called _bri_down with the command shown above, and preset ID 7](perceptual-brightness-example-down-preset.png)

You can then apply these commands as button actions for both "short" and "long" actions by typing the preset ID into those boxes under *Settings>Time & Macros>Button Actions*, as shown below.

![A screenshot of the button configuration to apply preset 7 for both short and long actions to button 6](perceptual-brightness-example-down-button-action.png)

## Single button for brightness cycling
For a single button, a command is provided that goes between brightness steps in a single direction, or after a sort pause, will start changing in the other direction. This works great for a single button. That API command is `{"pb":"/"}` and you can assign this to a button exactly as the up and down buttons described earlier.

A variation of that command will loop back in the other direction even without the button being released. It starts cycling down once the max brightness is reached, and up again once the minimum brightness is reached. You use the API command `{"pb":"~"}` for the looping variation.

## Brightness changing speed
When holding down buttons, `WLED_LONG_REPEATED_ACTION` is the variable that defines how long it takes to call the command again with buttons, by default 400ms. You can customize this with a build flag such as `-D WLED_LONG_REPEATED_ACTION=500`, and you can also play around with the number of brightness steps.

## Other changes to brightness
This usermod doesn't keep track of changes made to brightness outside of these API commands. You can still change the brightness in other ways (eg, presets with `bri` specified, or changes to the brightness slider in the web UI), but when you use an increment/decrement/cycling command as described in this page, the usermod will proceed from the brightness step you last reached using those API commands.
8 changes: 8 additions & 0 deletions wled00/usermods_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@
#include "../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h"
#endif

#ifdef USERMOD_PERCEPTUAL_BRIGHTNESS
#include "../usermods/perceptual_brightness/perceptual_brightness.h"
#endif

#ifdef USERMOD_INTERNAL_TEMPERATURE
#include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h"
#endif
Expand Down Expand Up @@ -423,6 +427,10 @@ void registerUsermods()
UsermodManager::add(new AnimartrixUsermod("Animartrix", false));
#endif

#ifdef USERMOD_PERCEPTUAL_BRIGHTNESS
UsermodManager::add(new UsermodPerceptualBrightness());
#endif

#ifdef USERMOD_INTERNAL_TEMPERATURE
UsermodManager::add(new InternalTemperatureUsermod());
#endif
Expand Down

0 comments on commit 69bbbab

Please sign in to comment.