Skip to content

add: general Fade(from, to, duration) method #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 16, 2022
Merged
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
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