Skip to content

Commit

Permalink
Allow stop to turn led fully off regardless of min brightness (#121)
Browse files Browse the repository at this point in the history
release 4.13.0
  • Loading branch information
jandelgado authored Aug 20, 2023
1 parent 5b10437 commit 0633121
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 94 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# JLed changelog (github.com/jandelgado/jled)

## [2023-08-20] 4.13.0

* new: `Stop()` takes optional parameter allowing to turn LED fully off

## [2023-06-29] 4.12.2

* fix: `JLedSequence` starting again after call to `Stop` (https://github.com/jandelgado/jled/issues/115)
Expand Down
54 changes: 45 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ void loop() {
* [Arduino IDE](#arduino-ide)
* [PlatformIO](#platformio)
* [Usage](#usage)
* [Output pipeline](#output-pipeline)
* [Effects](#effects)
* [Static on and off](#static-on-and-off)
* [Static on example](#static-on-example)
Expand Down Expand Up @@ -157,6 +158,27 @@ the only argument. Further configuration of the LED object is done using a fluen
interface, e.g. `JLed led = JLed(13).Breathe(2000).DelayAfter(1000).Repeat(5)`.
See the examples section below for further details.

#### Output pipeline

First the configured effect (e.g. `Fade`) is evaluated for the current time
`t`. JLed internally uses unsigned bytes to represent brightness values,
ranging from 0 to 255. Next, the value is scaled to the limits set by
`MinBrightness` and `MaxBrightness` (optionally). When the effect is configured
for a low-active LED using `LowActive`, the brightness value will be inverted,
i.e., the value will be subtracted from 255. Finally the value is passed to the
hardware abstraction, which might scale it to the resolution used by the actual
device (e.g. 10 bits for an ESP8266). Finally the brightness value is written
out to the configure GPIO.

```
┌───────────┐ ┌────────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐ ┌────────┐
│ Evaluate │ │ Scale to │ │ Low │YES │ Invert │ │Scale for│ │Write to│
│ effect(t) ├───►│ [min, max] ├───►│ active? ├───►│ signal ├───►│Hardware ├───►│ GPIO │
└───────────┘ └────────────┘ └────┬────┘ └────────┘ └───▲─────┘ └────────┘
│ NO │
└───────────────────────────┘
```

### Effects

#### Static on and off
Expand All @@ -165,15 +187,15 @@ Calling `On(uint16_t period=1)` turns the LED on. To immediately turn a LED on,
make a call like `JLed(LED_BUILTIN).On().Update()`. The `period` is optional
and defaults to 1ms.

`Off()` works like `On()`, except that it turns the LED off, i.e. it sets the
`Off()` works like `On()`, except that it turns the LED off, i.e., it sets the
brightness to 0.

Use the `Set(uint8_t brightness, uint16_t period=1)` method to set the
brightness to the given value, i.e. `Set(255)` is equivalent to calling `On()`
brightness to the given value, i.e., `Set(255)` is equivalent to calling `On()`
and `Set(0)` is equivalent to calling `Off()`.

Technically, `Set`, `On` and `Off` are effects with a default period of 1ms, that
set the brightness to a constant value. Specifiying a different period has an
set the brightness to a constant value. Specifying a different period has an
effect on when the `Update()` method will be done updating the effect and
return false (like for any other effects). This is important when for example
in a `JLedSequence` the LED should stay on for a given amount of time.
Expand Down Expand Up @@ -303,15 +325,15 @@ void loop() {

In FadeOff mode, the LED is smoothly faded off using PWM. The fade starts at
100% brightness. Internally it is implemented as a mirrored version of the
FadeOn function, i.e. FadeOff(t) = FadeOn(period-t). The `FadeOff()` method
FadeOn function, i.e., FadeOff(t) = FadeOn(period-t). The `FadeOff()` method
takes the period of the effect as argument.

#### Fade

The Fade effect allows to fade from any start value `from` to any target value
`to` with the given duration. Internally it sets up a `FadeOn` or `FadeOff`
effect and `MinBrightness` and `MaxBrightness` values properly. The `Fade`
method take three argumens: `from`, `to` and `duration`.
method take three arguments: `from`, `to` and `duration`.

<a href="examples/fade_from_to"><img alt="fade from-to" src="doc/fade_from-to.png" height=200></a>

Expand Down Expand Up @@ -403,15 +425,29 @@ you want to start-over an effect.
##### Immediate Stop
Call `Stop()` to immediately turn the LED off and stop any running effects.
Further calls to `Update()` will have no effect unless the Led is reset (using
`Reset()`) or a new effect activated.
Further calls to `Update()` will have no effect, unless the Led is reset using
`Reset()` or a new effect is activated. By default, `Stop()` sets the current
brightness level to `MinBrightness`.
`Stop()` takes an optional argument `mode` of type `JLed::eStopMode`:
* if set to `JLed::eStopMode::KEEP_CURRENT`, the LEDs current level will be kept
* if set to `JLed::eStopMode::FULL_OFF` the level of the LED is set to `0`,
regardless of what `MinBrightness` is set to, effectively turning the LED off
* if set to `JLed::eStopMode::TO_MIN_BRIGHTNESS` (default behavior), the LED
will set to the value of `MinBrightness`
```c++
// stop the effect and set the brightness level to 0, regardless of min brightness
led.Stop(JLed::eStopMode::FULL_OFF);
```

#### Misc functions

##### Low active for inverted output

Use the `LowActive()` method when the connected LED is low active. All output
will be inverted by JLed (i.e. instead of x, the value of 255-x will be set).
will be inverted by JLed (i.e., instead of x, the value of 255-x will be set).

##### Minimum- and Maximum brightness level

Expand Down Expand Up @@ -502,7 +538,7 @@ src_dir = examples/multiled_mbed

The DAC of the ESP8266 operates with 10 bits, every value JLed writes out gets
automatically scaled to 10 bits, since JLed internally only uses 8 bits. The
scaling methods make sure that min/max relationships are preserved, i.e. 0 is
scaling methods make sure that min/max relationships are preserved, i.e., 0 is
mapped to 0 and 255 is mapped to 1023. When using a user-defined brightness
function on the ESP8266, 8-bit values must be returned, all scaling is done by
JLed transparently for the application, yielding platform-independent code.
Expand Down
2 changes: 1 addition & 1 deletion library.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "JLed",
"version": "4.12.2",
"version": "4.13.0",
"description": "An embedded library to control LEDs",
"license": "MIT",
"frameworks": ["espidf", "arduino", "mbed"],
Expand Down
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=JLed
version=4.12.2
version=4.13.0
author=Jan Delgado <jdelgado[at]gmx.net>
maintainer=Jan Delgado <jdelgado[at]gmx.net>
sentence=An Arduino library to control LEDs
Expand Down
52 changes: 33 additions & 19 deletions src/jled_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,15 @@ class TJLed {
// Hardware abstraction giving access to the MCU
HalType hal_;

// Evaluate effect(t) and scale to be within [minBrightness, maxBrightness]
// assumes brigthness_eval_ is set as it is not checked here.
uint8_t Eval(uint32_t t) const {
const auto val = brightness_eval_->Eval(t);
return lerp8by8(val, minBrightness_, maxBrightness_);
}

// Write val out to "hardware", reverting signal when active-low is set.
void Write(uint8_t val) {
val = lerp8by8(val, minBrightness_, maxBrightness_);
hal_.analogWrite(IsLowActive() ? kFullBrightness - val : val);
}

Expand Down Expand Up @@ -333,8 +340,12 @@ class TJLed {

// Stop current effect and turn LED immeadiately off. Further calls to
// Update() will have no effect.
B& Stop() {
Write(kZeroBrightness);
enum eStopMode { TO_MIN_BRIGHTNESS = 0, FULL_OFF, KEEP_CURRENT };
B& Stop(eStopMode mode = eStopMode::TO_MIN_BRIGHTNESS) {
if (mode != eStopMode::KEEP_CURRENT) {
Write(mode == eStopMode::FULL_OFF ? kZeroBrightness
: minBrightness_);
}
state_ = ST_STOPPED;
return static_cast<B&>(*this);
}
Expand Down Expand Up @@ -403,31 +414,34 @@ class TJLed {
const auto period = brightness_eval_->Period();
const auto t = (now - time_start_) % (period + delay_after_);

if (!IsForever()) {
const auto time_end =
time_start_ +
(uint32_t)(period + delay_after_) * num_repetitions_ - 1;

if ((int32_t)(now - time_end) >= 0) {
// make sure final value of t = (period-1) is set
state_ = ST_STOPPED;
const auto val = Eval(period - 1);
Write(val);
return false;
}
}

if (t < period) {
state_ = ST_RUNNING;
Write(brightness_eval_->Eval(t));
Write(Eval(t));
return true;
} else {
if (state_ == ST_RUNNING) {
// when in delay after phase, just call Write()
// once at the beginning.
state_ = ST_IN_DELAY_AFTER_PHASE;
Write(brightness_eval_->Eval(period - 1));
Write(Eval(period - 1));
return true;
}
}

if (IsForever()) return true;

const auto time_end =
time_start_ + (uint32_t)(period + delay_after_) * num_repetitions_ -
1;

if ((int32_t)(now - time_end) >= 0) {
// make sure final value of t = (period-1) is set
state_ = ST_STOPPED;
Write(brightness_eval_->Eval(period - 1));
return false;
}
return true;
return false;
}

B& SetBrightnessEval(BrightnessEvaluator* be) {
Expand Down
2 changes: 1 addition & 1 deletion test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ bin:
mkdir -p bin

clean: phony
rm -f {*.gcov,*.gcda,*.gcno,*.o} .depend
rm -f {./,esp-idf,esp-idf/driver}/{*.gcov,*.gcda,*.gcno,*.o} .depend

clobber: clean
rm -f bin/*
Expand Down
110 changes: 47 additions & 63 deletions test/test_jled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,34 +323,61 @@ TEST_CASE("Stop() stops the effect", "[jled]") {
auto eval = MockBrightnessEvaluator(ByteVec{255, 255, 255, 0});
TestJLed jled = TestJLed(10).UserFunc(&eval);

CHECK(jled.IsRunning());
REQUIRE(jled.IsRunning());
jled.Update();
CHECK(jled.Hal().Value() == 255);
jled.Stop();

CHECK(!jled.IsRunning());
CHECK_FALSE(jled.Update());
CHECK(0 == jled.Hal().Value());
}

TEST_CASE("default Stop() sets the brightness to minBrightness", "[jled]") {
auto eval = MockBrightnessEvaluator(ByteVec{100, 0});
TestJLed jled = TestJLed(10).UserFunc(&eval).MinBrightness(50);

jled.Update();
REQUIRE(130 == jled.Hal().Value()); // 100 scaled to [50,255]
jled.Stop();

CHECK(50 == jled.Hal().Value());
}

TEST_CASE("Stop(FULL_OFF) sets the brightness to 0", "[jled]") {
auto eval = MockBrightnessEvaluator(ByteVec{100, 0});
TestJLed jled = TestJLed(10).UserFunc(&eval).MinBrightness(50);

jled.Update();
REQUIRE(130 == jled.Hal().Value()); // 100 scaled to [50,255]
jled.Stop(TestJLed::eStopMode::FULL_OFF);

// update must not change anything
CHECK_FALSE(jled.Update());
CHECK(0 == jled.Hal().Value());
}

TEST_CASE("Stop(KEEP_CURRENT) keeps the last brightness level", "[jled]") {
auto eval = MockBrightnessEvaluator(ByteVec{100, 101});
TestJLed jled = TestJLed(10).UserFunc(&eval).MinBrightness(50);

jled.Update();
REQUIRE(130 == jled.Hal().Value()); // 100 scaled to [50,255]
jled.Stop(TestJLed::eStopMode::KEEP_CURRENT);

CHECK(130 == jled.Hal().Value());
}

TEST_CASE("LowActive() inverts signal", "[jled]") {
auto eval = MockBrightnessEvaluator(ByteVec{255});
TestJLed jled = TestJLed(10).UserFunc(&eval).LowActive();
auto eval = MockBrightnessEvaluator(ByteVec{0, 255});
TestJLed jled = TestJLed(1).UserFunc(&eval).LowActive();

CHECK(jled.IsLowActive());

jled.Update();
CHECK(0 == jled.Hal().Value());

jled.Stop();
CHECK(255 == jled.Hal().Value());

jled.Hal().SetMillis(1);
jled.Update();
CHECK(0 == jled.Hal().Value());
}

TEST_CASE("effect with repeat 2 runs twice as long", "[jled]") {
TEST_CASE("effect with repeat 2 repeats sequence once", "[jled]") {
auto eval = MockBrightnessEvaluator(ByteVec{10, 20});
TestJLed jled = TestJLed(10).UserFunc(&eval).Repeat(2);

Expand Down Expand Up @@ -483,67 +510,24 @@ TEST_CASE("Previously set min brightness level can be read back", "[jled]") {
}

TEST_CASE(
"Setting min and max brightness levels limits brightness value written to "
"HAL",
"Setting min and max brightness levels scales evaluated effect values",
"[jled]") {
class TestableJLed : public TestJLed {
public:
using TestJLed::TestJLed;
static void test() {
SECTION(
"After setting max brightness to 0, 0 is always written to the "
"HAL",
"max level is 0") {
TestableJLed jled(1);

jled.MaxBrightness(0);

for (auto b = 0; b <= 255; b++) {
jled.Write(b);
CHECK(0 == jled.Hal().Value());
}
}

SECTION(
"After setting max brightness to 255, the original value is "
"written to the HAL",
"max level is 255") {
TestableJLed jled(1);

jled.MaxBrightness(255);

for (auto b = 0; b <= 255; b++) {
jled.Write(b);
CHECK(b == jled.Hal().Value());
}
}

SECTION(
"After setting min brightness, the original value is at least "
"at this level") {
TestableJLed jled(1);

jled.MinBrightness(100);
jled.Write(0);
CHECK(100 == jled.Hal().Value());
}
TestableJLed jled(1);

SECTION(
"After setting min and max brightness, the original value "
"scaled"
"to this interval") {
TestableJLed jled(1);
auto eval = MockBrightnessEvaluator(ByteVec{0, 128, 255});
jled.UserFunc(&eval).MinBrightness(100).MaxBrightness(200);

jled.MinBrightness(100).MaxBrightness(200);
jled.Write(0);
CHECK(100 == jled.Hal().Value());
jled.Write(255);
CHECK(200 == jled.Hal().Value());
}
CHECK(100 == jled.Eval(0));
CHECK(150 == jled.Eval(1));
CHECK(200 == jled.Eval(2));
}
};
TestableJLed::test();
}
};

TEST_CASE("timeChangeSinceLastUpdate detects time changes", "[jled]") {
class TestableJLed : public TestJLed {
Expand Down

0 comments on commit 0633121

Please sign in to comment.