Skip to content

Commit

Permalink
add: general Fade(from, to, duration) method (#106)
Browse files Browse the repository at this point in the history
* FadeOn/FadeOff now use generalized Breathe evaluator allowing us to get rid of classes formerly used for FadeOn/FadeOff
* add generalized Fade(from,to,duration) function which internally configures the breathe-effect with min- and
max-brightness correctly set up.
* add fade from-to example
* add pule example
  • Loading branch information
jandelgado committed Nov 16, 2022
1 parent 7bf7edc commit 4f8de1f
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 85 deletions.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ void loop() {
* [FadeOn](#fadeon)
* [FadeOn example](#fadeon-example)
* [FadeOff](#fadeoff)
* [Fade](#fade)
* [Fade example](#fade-example)
* [User provided brightness function](#user-provided-brightness-function)
* [User provided brightness function example](#user-provided-brightness-function-example)
* [Delays and repetitions](#delays-and-repetitions)
Expand Down Expand Up @@ -296,9 +298,33 @@ 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. FadeOn(t) = FadeOff(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`.

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

##### Fade example

```c++
#include <jled.h>

// fade from 100 to 200 with period 1000
auto led = JLed(9).Fade(100, 200, 1000);

void setup() { }

void loop() {
led.Update();
}
```

#### User provided brightness function

It is also possible to provide a user defined brightness evaluator. The class
Expand Down Expand Up @@ -545,6 +571,8 @@ Example sketches are provided in the [examples](examples/) directory.
* [Candle effect](examples/candle)
* [Fade LED on](examples/fade_on)
* [Fade LED off](examples/fade_off)
* [Fade from-to effect](examples/fade_from_to)
* [Pulse effect](examples/pulse)
* [Controlling multiple LEDs in parallel](examples/multiled)
* [Controlling multiple LEDs in parallel (mbed)](examples/multiled_mbed)
* [Controlling multiple LEDs sequentially](examples/sequence)
Expand Down
Binary file modified doc/cheat_sheet.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/fade_from-to.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions examples/fade_from_to/fade_from_to.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// JLed fade from-to example. Example randomly fades to a new level with
// a random duration.
// Copyright 2022 by Jan Delgado. All rights reserved.
// https://github.com/jandelgado/jled
#include <jled.h>

auto led = JLed(5).On(1); // start with LED turned on

void setup() {}

void loop() {
static uint8_t last_to = 255;

if (!led.Update()) {
// when effect is done (Update() returns false),
// reconfigure fade effect using random values
auto new_from = last_to;
auto new_to = jled::rand8();
auto duration = 250 + jled::rand8() * 4;
last_to = new_to;
led.Fade(new_from, new_to, duration).Repeat(1);
}
}
5 changes: 3 additions & 2 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ default_envs = esp32
;default_envs = sparkfun_samd21_dev_usb

; uncomment example to build
;src_dir = examples/hello
src_dir = examples/hello
;src_dir = examples/morse
;src_dir = examples/breathe
;src_dir = examples/candle
Expand All @@ -33,7 +33,8 @@ default_envs = esp32
;src_dir = examples/user_func
;src_dir = examples/sequence
;src_dir = examples/custom_hal
src_dir = examples/pulse
;src_dir = examples/pulse
;src_dir = examples/fade_from_to

[env:nanoatmega328]
platform = atmelavr
Expand Down
65 changes: 24 additions & 41 deletions src/jled_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static constexpr uint8_t kZeroBrightness = 0;

uint8_t fadeon_func(uint32_t t, uint16_t period);
uint8_t rand8();
void rand_seed(uint32_t s);
void rand_seed(uint32_t s);
uint8_t scale8(uint8_t val, uint8_t f);
uint8_t lerp8by8(uint8_t val, uint8_t a, uint8_t b);

Expand Down Expand Up @@ -97,36 +97,6 @@ class BlinkBrightnessEvaluator : public CloneableBrightnessEvaluator {
}
};

// fade LED on
class FadeOnBrightnessEvaluator : public CloneableBrightnessEvaluator {
uint16_t period_;

public:
FadeOnBrightnessEvaluator() = delete;
explicit FadeOnBrightnessEvaluator(uint16_t period) : period_(period) {}
BrightnessEvaluator* clone(void* ptr) const override {
return new (ptr) FadeOnBrightnessEvaluator(*this);
}
uint16_t Period() const override { return period_; }
uint8_t Eval(uint32_t t) const override { return fadeon_func(t, period_); }
};

// fade LED off
class FadeOffBrightnessEvaluator : public CloneableBrightnessEvaluator {
uint16_t period_;

public:
FadeOffBrightnessEvaluator() = delete;
explicit FadeOffBrightnessEvaluator(uint16_t period) : period_(period) {}
BrightnessEvaluator* clone(void* ptr) const override {
return new (ptr) FadeOffBrightnessEvaluator(*this);
}
uint16_t Period() const override { return period_; }
uint8_t Eval(uint32_t t) const override {
return fadeon_func(period_ - t, period_);
}
};

// The breathe func is composed by fade-on, on and fade-off phases. For fading
// we approximate the following function:
// y(x) = exp(sin((t-period/4.) * 2. * PI / period)) - 0.36787944) * 108.)
Expand Down Expand Up @@ -160,6 +130,10 @@ class BreatheBrightnessEvaluator : public CloneableBrightnessEvaluator {
else
return fadeon_func(Period() - t, duration_fade_off_);
}

uint16_t DurationFadeOn() const { return duration_fade_on_; }
uint16_t DurationFadeOff() const { return duration_fade_off_; }
uint16_t DurationOn() const { return duration_on_; }
};

class CandleBrightnessEvaluator : public CloneableBrightnessEvaluator {
Expand Down Expand Up @@ -281,14 +255,25 @@ class TJLed {

// Fade LED on
B& FadeOn(uint16_t duration) {
return SetBrightnessEval(new (brightness_eval_buf_)
FadeOnBrightnessEvaluator(duration));
return SetBrightnessEval(new (
brightness_eval_buf_) BreatheBrightnessEvaluator(duration, 0, 0));
}

// Fade LED off - acutally is just inverted version of FadeOn()
B& FadeOff(uint16_t duration) {
return SetBrightnessEval(new (brightness_eval_buf_)
FadeOffBrightnessEvaluator(duration));
return SetBrightnessEval(new (
brightness_eval_buf_) BreatheBrightnessEvaluator(0, 0, duration));
}

// Fade from "from" to "to" with period "duration". Sets up the breathe
// effect with the proper parameters and sets Min/Max brightness to reflect
// levels specified by "from" and "to".
B& Fade(uint8_t from, uint8_t to, uint16_t duration) {
if (from < to) {
return FadeOn(duration).MinBrightness(from).MaxBrightness(to);
} else {
return FadeOff(duration).MinBrightness(to).MaxBrightness(from);
}
}

// Set effect to Breathe, with the given period time in ms.
Expand Down Expand Up @@ -388,9 +373,7 @@ class TJLed {
return (now & 255) != last_update_time_;
}

void trackLastUpdateTime(uint32_t t) {
last_update_time_ = (t & 255);
}
void trackLastUpdateTime(uint32_t t) { last_update_time_ = (t & 255); }

// update brightness of LED using the given brightness evaluator
// (brightness) ________________
Expand All @@ -405,8 +388,8 @@ class TJLed {
if (state_ == ST_STOPPED || !brightness_eval_) return false;

if (state_ == ST_INIT) {
time_start_ = now + delay_before_;
state_ = ST_RUNNING;
time_start_ = now + delay_before_;
state_ = ST_RUNNING;
} else {
// no need to process updates twice during one time tick.
if (!timeChangedSinceLastUpdate(now)) return true;
Expand Down Expand Up @@ -454,7 +437,7 @@ class TJLed {
}

public:
// Number of bits used to control brightness with Min/MaxBrightness().
// Number of bits used to control brightness with Min/MaxBrightness().
static constexpr uint8_t kBitsBrightness = 8;
static constexpr uint8_t kBrightnessStep = 1;

Expand Down
98 changes: 57 additions & 41 deletions test/test_jled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ using jled::BreatheBrightnessEvaluator;
using jled::BrightnessEvaluator;
using jled::CandleBrightnessEvaluator;
using jled::ConstantBrightnessEvaluator;
using jled::FadeOffBrightnessEvaluator;
using jled::FadeOnBrightnessEvaluator;
using jled::TJLed;

// TestJLed is a JLed class using the HalMock for tests. This allows to
Expand Down Expand Up @@ -112,9 +110,14 @@ TEST_CASE("using Breathe() configures BreatheBrightnessEvaluator", "[jled]") {
using TestJLed::TestJLed;
static void test() {
TestableJLed jled(1);
jled.Breathe(0);
jled.Breathe(100, 200, 300);
REQUIRE(dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_) != nullptr);
auto eval = dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_);
CHECK(100 == eval->DurationFadeOn());
CHECK(200 == eval->DurationOn());
CHECK(300 == eval->DurationFadeOff());
}
};
TestableJLed::test();
Expand All @@ -140,23 +143,68 @@ TEST_CASE("using Fadeon(), FadeOff() configures Fade-BrightnessEvaluators",
public:
using TestJLed::TestJLed;
static void test() {
SECTION("FadeOff() initializes with FadeOffBrightnessEvaluator") {
SECTION("FadeOff() initializes with BreatheBrightnessEvaluator") {
TestableJLed jled(1);
jled.FadeOff(0);
REQUIRE(dynamic_cast<FadeOffBrightnessEvaluator *>(
jled.FadeOff(100);
REQUIRE(dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_) != nullptr);
auto eval = dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_);
CHECK(0 == eval->DurationFadeOn());
CHECK(0 == eval->DurationOn());
CHECK(100 == eval->DurationFadeOff());
}
SECTION("FadeOn() initializes with FadeOnBrightnessEvaluator") {
SECTION("FadeOn() initializes with BreatheBrightnessEvaluator") {
TestableJLed jled(1);
jled.FadeOn(0);
REQUIRE(dynamic_cast<FadeOnBrightnessEvaluator *>(
jled.FadeOn(100);
REQUIRE(dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_) != nullptr);
auto eval = dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_);
CHECK(100 == eval->DurationFadeOn());
CHECK(0 == eval->DurationOn());
CHECK(0 == eval->DurationFadeOff());
}
}
};
TestableJLed::test();
}

TEST_CASE("using Fade() configures BreatheBrightnessEvaluator", "[jled]") {
class TestableJLed : public TestJLed {
public:
using TestJLed::TestJLed;
static void test() {
SECTION("fade with from < to") {
TestableJLed jled(1);
jled.Fade(100, 200, 300);
REQUIRE(dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_) != nullptr);
auto eval = dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_);
CHECK(300 == eval->DurationFadeOn());
CHECK(0 == eval->DurationOn());
CHECK(0 == eval->DurationFadeOff());
CHECK(100 == jled.MinBrightness());
CHECK(200 == jled.MaxBrightness());
}
SECTION("fade with from >= to") {
TestableJLed jled(1);
jled.Fade(200, 100, 300);
REQUIRE(dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_) != nullptr);
auto eval = dynamic_cast<BreatheBrightnessEvaluator *>(
jled.brightness_eval_);
CHECK(0 == eval->DurationFadeOn());
CHECK(0 == eval->DurationOn());
CHECK(300 == eval->DurationFadeOff());
CHECK(100 == jled.MinBrightness());
CHECK(200 == jled.MaxBrightness());
}
}
};
TestableJLed::test();
}
TEST_CASE("UserFunc() allows to use a custom brightness evaluator", "[jled]") {
class TestableJLed : public TestJLed {
public:
Expand Down Expand Up @@ -205,38 +253,6 @@ TEST_CASE("CandleBrightnessEvaluator simulated candle flickering", "[jled]") {
CHECK(eval.Eval(999) > 0);
}

TEST_CASE("FadeOnEvaluator evaluates to expected brightness curve", "[jled]") {
constexpr auto kPeriod = 2000;

auto evalOn = FadeOnBrightnessEvaluator(kPeriod);

CHECK(kPeriod == evalOn.Period());

const std::map<uint32_t, uint8_t> test_values = {
{0, 0}, {500, 13}, {1000, 68}, {1500, 179},
{1999, 255}, {2000, 255}, {10000, 255}};

for (const auto &x : test_values) {
CHECK(x.second == evalOn.Eval(x.first));
}
}

TEST_CASE("FadeOffEvaluator evaluates to expected brightness curve", "[jled]") {
constexpr auto kPeriod = 2000;

// note: FadeOff is invervted FadeOn
auto evalOff = FadeOffBrightnessEvaluator(kPeriod);

CHECK(kPeriod == evalOff.Period());
const std::map<uint32_t, uint8_t> test_values = {
{0, 0}, {500, 13}, {1000, 68}, {1500, 179},
{1999, 255}, {2000, 255}, {10000, 255}};

for (const auto &x : test_values) {
CHECK(x.second == evalOff.Eval(kPeriod - x.first));
}
}

TEST_CASE(
"BreatheEvaluator evaluates to bell curve distributed brightness curve",
"[jled]") {
Expand Down

0 comments on commit 4f8de1f

Please sign in to comment.