Skip to content

Commit 0633121

Browse files
authored
Allow stop to turn led fully off regardless of min brightness (#121)
release 4.13.0
1 parent 5b10437 commit 0633121

File tree

7 files changed

+132
-94
lines changed

7 files changed

+132
-94
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# JLed changelog (github.com/jandelgado/jled)
22

3+
## [2023-08-20] 4.13.0
4+
5+
* new: `Stop()` takes optional parameter allowing to turn LED fully off
6+
37
## [2023-06-29] 4.12.2
48

59
* fix: `JLedSequence` starting again after call to `Stop` (https://github.com/jandelgado/jled/issues/115)

README.md

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ void loop() {
5252
* [Arduino IDE](#arduino-ide)
5353
* [PlatformIO](#platformio)
5454
* [Usage](#usage)
55+
* [Output pipeline](#output-pipeline)
5556
* [Effects](#effects)
5657
* [Static on and off](#static-on-and-off)
5758
* [Static on example](#static-on-example)
@@ -157,6 +158,27 @@ the only argument. Further configuration of the LED object is done using a fluen
157158
interface, e.g. `JLed led = JLed(13).Breathe(2000).DelayAfter(1000).Repeat(5)`.
158159
See the examples section below for further details.
159160

161+
#### Output pipeline
162+
163+
First the configured effect (e.g. `Fade`) is evaluated for the current time
164+
`t`. JLed internally uses unsigned bytes to represent brightness values,
165+
ranging from 0 to 255. Next, the value is scaled to the limits set by
166+
`MinBrightness` and `MaxBrightness` (optionally). When the effect is configured
167+
for a low-active LED using `LowActive`, the brightness value will be inverted,
168+
i.e., the value will be subtracted from 255. Finally the value is passed to the
169+
hardware abstraction, which might scale it to the resolution used by the actual
170+
device (e.g. 10 bits for an ESP8266). Finally the brightness value is written
171+
out to the configure GPIO.
172+
173+
```
174+
┌───────────┐ ┌────────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐ ┌────────┐
175+
│ Evaluate │ │ Scale to │ │ Low │YES │ Invert │ │Scale for│ │Write to│
176+
│ effect(t) ├───►│ [min, max] ├───►│ active? ├───►│ signal ├───►│Hardware ├───►│ GPIO │
177+
└───────────┘ └────────────┘ └────┬────┘ └────────┘ └───▲─────┘ └────────┘
178+
│ NO │
179+
└───────────────────────────┘
180+
```
181+
160182
### Effects
161183

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

168-
`Off()` works like `On()`, except that it turns the LED off, i.e. it sets the
190+
`Off()` works like `On()`, except that it turns the LED off, i.e., it sets the
169191
brightness to 0.
170192

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

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

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

309331
#### Fade
310332

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

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

@@ -403,15 +425,29 @@ you want to start-over an effect.
403425
##### Immediate Stop
404426
405427
Call `Stop()` to immediately turn the LED off and stop any running effects.
406-
Further calls to `Update()` will have no effect unless the Led is reset (using
407-
`Reset()`) or a new effect activated.
428+
Further calls to `Update()` will have no effect, unless the Led is reset using
429+
`Reset()` or a new effect is activated. By default, `Stop()` sets the current
430+
brightness level to `MinBrightness`.
431+
432+
`Stop()` takes an optional argument `mode` of type `JLed::eStopMode`:
433+
434+
* if set to `JLed::eStopMode::KEEP_CURRENT`, the LEDs current level will be kept
435+
* if set to `JLed::eStopMode::FULL_OFF` the level of the LED is set to `0`,
436+
regardless of what `MinBrightness` is set to, effectively turning the LED off
437+
* if set to `JLed::eStopMode::TO_MIN_BRIGHTNESS` (default behavior), the LED
438+
will set to the value of `MinBrightness`
439+
440+
```c++
441+
// stop the effect and set the brightness level to 0, regardless of min brightness
442+
led.Stop(JLed::eStopMode::FULL_OFF);
443+
```
408444

409445
#### Misc functions
410446

411447
##### Low active for inverted output
412448

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

416452
##### Minimum- and Maximum brightness level
417453

@@ -502,7 +538,7 @@ src_dir = examples/multiled_mbed
502538

503539
The DAC of the ESP8266 operates with 10 bits, every value JLed writes out gets
504540
automatically scaled to 10 bits, since JLed internally only uses 8 bits. The
505-
scaling methods make sure that min/max relationships are preserved, i.e. 0 is
541+
scaling methods make sure that min/max relationships are preserved, i.e., 0 is
506542
mapped to 0 and 255 is mapped to 1023. When using a user-defined brightness
507543
function on the ESP8266, 8-bit values must be returned, all scaling is done by
508544
JLed transparently for the application, yielding platform-independent code.

library.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "JLed",
3-
"version": "4.12.2",
3+
"version": "4.13.0",
44
"description": "An embedded library to control LEDs",
55
"license": "MIT",
66
"frameworks": ["espidf", "arduino", "mbed"],

library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name=JLed
2-
version=4.12.2
2+
version=4.13.0
33
author=Jan Delgado <jdelgado[at]gmx.net>
44
maintainer=Jan Delgado <jdelgado[at]gmx.net>
55
sentence=An Arduino library to control LEDs

src/jled_base.h

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,15 @@ class TJLed {
180180
// Hardware abstraction giving access to the MCU
181181
HalType hal_;
182182

183+
// Evaluate effect(t) and scale to be within [minBrightness, maxBrightness]
184+
// assumes brigthness_eval_ is set as it is not checked here.
185+
uint8_t Eval(uint32_t t) const {
186+
const auto val = brightness_eval_->Eval(t);
187+
return lerp8by8(val, minBrightness_, maxBrightness_);
188+
}
189+
190+
// Write val out to "hardware", reverting signal when active-low is set.
183191
void Write(uint8_t val) {
184-
val = lerp8by8(val, minBrightness_, maxBrightness_);
185192
hal_.analogWrite(IsLowActive() ? kFullBrightness - val : val);
186193
}
187194

@@ -333,8 +340,12 @@ class TJLed {
333340

334341
// Stop current effect and turn LED immeadiately off. Further calls to
335342
// Update() will have no effect.
336-
B& Stop() {
337-
Write(kZeroBrightness);
343+
enum eStopMode { TO_MIN_BRIGHTNESS = 0, FULL_OFF, KEEP_CURRENT };
344+
B& Stop(eStopMode mode = eStopMode::TO_MIN_BRIGHTNESS) {
345+
if (mode != eStopMode::KEEP_CURRENT) {
346+
Write(mode == eStopMode::FULL_OFF ? kZeroBrightness
347+
: minBrightness_);
348+
}
338349
state_ = ST_STOPPED;
339350
return static_cast<B&>(*this);
340351
}
@@ -403,31 +414,34 @@ class TJLed {
403414
const auto period = brightness_eval_->Period();
404415
const auto t = (now - time_start_) % (period + delay_after_);
405416

417+
if (!IsForever()) {
418+
const auto time_end =
419+
time_start_ +
420+
(uint32_t)(period + delay_after_) * num_repetitions_ - 1;
421+
422+
if ((int32_t)(now - time_end) >= 0) {
423+
// make sure final value of t = (period-1) is set
424+
state_ = ST_STOPPED;
425+
const auto val = Eval(period - 1);
426+
Write(val);
427+
return false;
428+
}
429+
}
430+
406431
if (t < period) {
407432
state_ = ST_RUNNING;
408-
Write(brightness_eval_->Eval(t));
433+
Write(Eval(t));
434+
return true;
409435
} else {
410436
if (state_ == ST_RUNNING) {
411437
// when in delay after phase, just call Write()
412438
// once at the beginning.
413439
state_ = ST_IN_DELAY_AFTER_PHASE;
414-
Write(brightness_eval_->Eval(period - 1));
440+
Write(Eval(period - 1));
441+
return true;
415442
}
416443
}
417-
418-
if (IsForever()) return true;
419-
420-
const auto time_end =
421-
time_start_ + (uint32_t)(period + delay_after_) * num_repetitions_ -
422-
1;
423-
424-
if ((int32_t)(now - time_end) >= 0) {
425-
// make sure final value of t = (period-1) is set
426-
state_ = ST_STOPPED;
427-
Write(brightness_eval_->Eval(period - 1));
428-
return false;
429-
}
430-
return true;
444+
return false;
431445
}
432446

433447
B& SetBrightnessEval(BrightnessEvaluator* be) {

test/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ bin:
9393
mkdir -p bin
9494

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

9898
clobber: clean
9999
rm -f bin/*

test/test_jled.cpp

Lines changed: 47 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -323,34 +323,61 @@ TEST_CASE("Stop() stops the effect", "[jled]") {
323323
auto eval = MockBrightnessEvaluator(ByteVec{255, 255, 255, 0});
324324
TestJLed jled = TestJLed(10).UserFunc(&eval);
325325

326-
CHECK(jled.IsRunning());
326+
REQUIRE(jled.IsRunning());
327327
jled.Update();
328-
CHECK(jled.Hal().Value() == 255);
329328
jled.Stop();
330329

331330
CHECK(!jled.IsRunning());
332-
CHECK_FALSE(jled.Update());
333-
CHECK(0 == jled.Hal().Value());
331+
}
332+
333+
TEST_CASE("default Stop() sets the brightness to minBrightness", "[jled]") {
334+
auto eval = MockBrightnessEvaluator(ByteVec{100, 0});
335+
TestJLed jled = TestJLed(10).UserFunc(&eval).MinBrightness(50);
336+
337+
jled.Update();
338+
REQUIRE(130 == jled.Hal().Value()); // 100 scaled to [50,255]
339+
jled.Stop();
340+
341+
CHECK(50 == jled.Hal().Value());
342+
}
343+
344+
TEST_CASE("Stop(FULL_OFF) sets the brightness to 0", "[jled]") {
345+
auto eval = MockBrightnessEvaluator(ByteVec{100, 0});
346+
TestJLed jled = TestJLed(10).UserFunc(&eval).MinBrightness(50);
347+
348+
jled.Update();
349+
REQUIRE(130 == jled.Hal().Value()); // 100 scaled to [50,255]
350+
jled.Stop(TestJLed::eStopMode::FULL_OFF);
334351

335-
// update must not change anything
336-
CHECK_FALSE(jled.Update());
337352
CHECK(0 == jled.Hal().Value());
338353
}
339354

355+
TEST_CASE("Stop(KEEP_CURRENT) keeps the last brightness level", "[jled]") {
356+
auto eval = MockBrightnessEvaluator(ByteVec{100, 101});
357+
TestJLed jled = TestJLed(10).UserFunc(&eval).MinBrightness(50);
358+
359+
jled.Update();
360+
REQUIRE(130 == jled.Hal().Value()); // 100 scaled to [50,255]
361+
jled.Stop(TestJLed::eStopMode::KEEP_CURRENT);
362+
363+
CHECK(130 == jled.Hal().Value());
364+
}
365+
340366
TEST_CASE("LowActive() inverts signal", "[jled]") {
341-
auto eval = MockBrightnessEvaluator(ByteVec{255});
342-
TestJLed jled = TestJLed(10).UserFunc(&eval).LowActive();
367+
auto eval = MockBrightnessEvaluator(ByteVec{0, 255});
368+
TestJLed jled = TestJLed(1).UserFunc(&eval).LowActive();
343369

344370
CHECK(jled.IsLowActive());
345371

346372
jled.Update();
347-
CHECK(0 == jled.Hal().Value());
348-
349-
jled.Stop();
350373
CHECK(255 == jled.Hal().Value());
374+
375+
jled.Hal().SetMillis(1);
376+
jled.Update();
377+
CHECK(0 == jled.Hal().Value());
351378
}
352379

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

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

485512
TEST_CASE(
486-
"Setting min and max brightness levels limits brightness value written to "
487-
"HAL",
513+
"Setting min and max brightness levels scales evaluated effect values",
488514
"[jled]") {
489515
class TestableJLed : public TestJLed {
490516
public:
491517
using TestJLed::TestJLed;
492518
static void test() {
493-
SECTION(
494-
"After setting max brightness to 0, 0 is always written to the "
495-
"HAL",
496-
"max level is 0") {
497-
TestableJLed jled(1);
498-
499-
jled.MaxBrightness(0);
500-
501-
for (auto b = 0; b <= 255; b++) {
502-
jled.Write(b);
503-
CHECK(0 == jled.Hal().Value());
504-
}
505-
}
506-
507-
SECTION(
508-
"After setting max brightness to 255, the original value is "
509-
"written to the HAL",
510-
"max level is 255") {
511-
TestableJLed jled(1);
512-
513-
jled.MaxBrightness(255);
514-
515-
for (auto b = 0; b <= 255; b++) {
516-
jled.Write(b);
517-
CHECK(b == jled.Hal().Value());
518-
}
519-
}
520-
521-
SECTION(
522-
"After setting min brightness, the original value is at least "
523-
"at this level") {
524-
TestableJLed jled(1);
525-
526-
jled.MinBrightness(100);
527-
jled.Write(0);
528-
CHECK(100 == jled.Hal().Value());
529-
}
519+
TestableJLed jled(1);
530520

531-
SECTION(
532-
"After setting min and max brightness, the original value "
533-
"scaled"
534-
"to this interval") {
535-
TestableJLed jled(1);
521+
auto eval = MockBrightnessEvaluator(ByteVec{0, 128, 255});
522+
jled.UserFunc(&eval).MinBrightness(100).MaxBrightness(200);
536523

537-
jled.MinBrightness(100).MaxBrightness(200);
538-
jled.Write(0);
539-
CHECK(100 == jled.Hal().Value());
540-
jled.Write(255);
541-
CHECK(200 == jled.Hal().Value());
542-
}
524+
CHECK(100 == jled.Eval(0));
525+
CHECK(150 == jled.Eval(1));
526+
CHECK(200 == jled.Eval(2));
543527
}
544528
};
545529
TestableJLed::test();
546-
}
530+
};
547531

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

0 commit comments

Comments
 (0)