diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 32425ee..4562c02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,11 +7,6 @@ on: - master name: run tests - -concurrency: - group: ${{ github.head_ref }} - cancel-in-progress: true - jobs: lint: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 45708db..5067819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # JLed changelog (github.com/jandelgado/jled) +## [2024-12-01] 4.15.0 + +* new: `Update()` methods now optionally return the last brightness value + calculated and written out to the LED. See `examples/last_brightness` + ## [2024-09-21] 4.14 -* new: make `Jled::Update(unit32_t t)` public, allowing optimizations and +* new: make `JLed::Update(unit32_t t)` public, allowing optimizations and simplified tests ## [2023-09-10] 4.13.1 diff --git a/README.md b/README.md index 39f01b6..8dd0639 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -
-Preferring Python? I just released jled-circuitpython, +
+Preferring Python? I just released jled-circuitpython, a JLed implementation for CircuitPython and MicroPython.
@@ -91,8 +91,8 @@ void loop() { * [Arduino framework](#arduino-framework) * [Raspberry Pi Pico](#raspberry-pi-pico) * [Example sketches](#example-sketches) - * [PlatformIO](#platformio-1) - * [Arduino IDE](#arduino-ide-1) + * [Building examples with PlatformIO](#building-examples-with-platformio) + * [Building examples with the Arduino IDE](#building-examples-with-the-arduino-ide) * [Extending](#extending) * [Support new hardware](#support-new-hardware) * [Unit tests](#unit-tests) @@ -194,9 +194,9 @@ 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()` and `Set(0)` is equivalent to calling `Off()`. -Technically, `Set`, `On` and `Off` are effects with a default period of 1ms, that +Technically, `Set`, `On` and `Off` are effects with a default period of 1ms, that 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 +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. @@ -268,25 +268,25 @@ auto led = JLed(13).Breathe(500, 1000, 500).DelayAfter(1000).Forever(); #### Candle -In candle mode, the random flickering of a candle or fire is simulated. +In candle mode, the random flickering of a candle or fire is simulated. The builder method has the following signature: `Candle(uint8_t speed, uint8_t jitter, uin16_t period)` -* `speed` - controls the speed of the effect. 0 for fastest, increasing speed +* `speed` - controls the speed of the effect. 0 for fastest, increasing speed divides into halve per increment. The default value is 7. * `jitter` - the amount of jittering. 0 none (constant on), 255 maximum. Default value is 15. * `period` - Period of effect in ms. The default value is 65535 ms. The default settings simulate a candle. For a fire effect for example use -call the method with `Candle(5 /*speed*/, 100 /* jitter*/)`. +call the method with `Candle(5 /*speed*/, 100 /* jitter*/)`. ##### Candle example ```c++ #include -// Candle on LED pin 13 (PWM capable). +// Candle on LED pin 13 (PWM capable). auto led = JLed(13).Candle(); void setup() { } @@ -363,7 +363,7 @@ two methods: as an unsigned byte, where 0 means LED off and 255 means full brightness. * `uint16_t Period() const` - period of the effect. -All time values are specified in milliseconds. +All time values are specified in milliseconds. The [user_func](examples/user_func) example demonstrates a simple user provided brightness function, while the [morse](examples/morse) example shows how a more @@ -410,10 +410,29 @@ specified by `DelayAfter()` method. ##### Update -Call `Update()` or `Update(uint32_t t)` periodically to update the state of the -LED. `Update` returns `true` if the effect is active, and `false` when it -finished. `Update()` is a shortcut to call `Update(uint32_t t)` with the -current time. +Call `Update(int16_t *pLast=nullptr)` or `Update(uint32_t t, int16_t *pLast=nullptr)` +to periodically update the state of the LED. + +`Update` returns `true`, if the effect is active, or `false` when it finished. +`Update()` is a shortcut to call `Update(uint32_t t)` with the current time in +milliseconds. + +To obtain the value of the last written brightness value (after applying min- +and max-brightness transformations), pass an additional optional pointer +`*pLast` , where this value will be stored, when it was written. Example: + +```c++ +int16_t lastVal = -1; +led.Update(&lastVal); +if (lastVal != -1) { + // the LED was updated with the brightness value now stored in lastVal + ... +} +``` + +Most of the time just calling `Update()` without any parameters is what you want. + +See [last_brightness](examples/last_brightness) example for a working example. ##### IsRunning @@ -453,8 +472,8 @@ will be inverted by JLed (i.e., instead of x, the value of 255-x will be set). ##### Minimum- and Maximum brightness level -The `MaxBrightness(uint8_t level)` method is used to set the maximum brightness -level of the LED. A level of 255 (the default) is full brightness, while 0 +The `MaxBrightness(uint8_t level)` method is used to set the maximum brightness +level of the LED. A level of 255 (the default) is full brightness, while 0 effectively turns the LED off. In the same way, the `MinBrightness(uint8_t level)` method sets the minimum brightness level. The default minimum level is 0. If minimum or maximum brightness levels are set, the output value is scaled to be @@ -462,7 +481,7 @@ within the interval defined by `[minimum brightness, maximum brightness]`: a value of 0 will be mapped to the minimum brightness level, a value of 255 will be mapped to the maximum brightness level. -The `uint_8 MaxBrightness() const` method returns the current maximum +The `uint_8 MaxBrightness() const` method returns the current maximum brightness level. `uint8_t MinBrightness() const` returns the current minimum brightness level. @@ -500,10 +519,10 @@ The `JLedSequence` provides the following methods: else `false`. * Use the `Repeat(n)` method to specify the number of repetitions. The default value is 1 repetition. The `Forever()` methods sets to repeat the sequence - forever. -* `Stop()` - turns off all `JLed` objects controlled by the sequence and + forever. +* `Stop()` - turns off all `JLed` objects controlled by the sequence and stops the sequence. Further calls to `Update()` will have no effect. -* `Reset()` - Resets all `JLed` objects controlled by the sequence and +* `Reset()` - Resets all `JLed` objects controlled by the sequence and the sequence, resulting in a start-over. ## Framework notes @@ -518,7 +537,7 @@ framework: platform=ststm32 board = nucleo_f401re framework = mbed -build_flags = -Isrc +build_flags = -Isrc src_filter = +<../../src/> +<./> upload_protocol=stlink ``` @@ -570,8 +589,8 @@ so it should be avoided and is normally not necessary. For completeness, the full signature of the Esp32Hal constructor is ``` -Esp32Hal(PinType pin, - int chan = kAutoSelectChan, +Esp32Hal(PinType pin, + int chan = kAutoSelectChan, uint16_t freq = 5000, ledc_timer_t timer = LEDC_TIMER_0) ``` @@ -600,9 +619,14 @@ necessary to upload sketches to the microcontroller. ### Raspberry Pi Pico -When using JLed on a Raspberry Pi Pico, the Pico-SDK and tools must be -installed. The Pico supports up to 16 PWM channels in parallel. See -the [pico-demo](examples/raspi_pico) for an example and build instructions. +When using JLed on a Raspberry Pi Pico, the Pico-SDK and tools can be +used. The Pico supports up to 16 PWM channels in parallel. See +the [pico-demo](examples/raspi_pico) for an example and build instructions when +the Pico-SDK is used. + +A probably easier approach is to use the Arduino platform. See +[platformio.ini](platformio.ini) for details (look for +`env:raspberrypi_pico_w`, which targets the Raspberry Pi Pico W. ## Example sketches @@ -621,6 +645,7 @@ Example sketches are provided in the [examples](examples/) directory. * [Controlling multiple LEDs sequentially](examples/sequence) * [Simple User provided effect](examples/user_func) * [Morsecode example](examples/morse) +* [Last brightness value example](examples/last_brightness) * [Custom HAL example](examples/custom_hal) * [Custom PCA9685 HAL](https://github.com/jandelgado/jled-pca9685-hal) * [Dynamically switch sequences](https://github.com/jandelgado/jled-example-switch-sequence) @@ -629,9 +654,9 @@ Example sketches are provided in the [examples](examples/) directory. * [ESP32 ESP-IDF example](https://github.com/jandelgado/jled-esp-idf-example) * [ESP32 ESP-IDF PlatformIO example](https://github.com/jandelgado/jled-esp-idf-platformio-example) -### PlatformIO +### Building examples with PlatformIO -To build an example using [the PlatformIO ide](http://platformio.org/), +To build an example using [the PlatformIO ide](http://platformio.org/), uncomment the example to be built in the [platformio.ini](platformio.ini) project file, e.g.: @@ -642,7 +667,7 @@ src_dir = examples/hello ;src_dir = examples/breathe ``` -### Arduino IDE +### Building examples with the Arduino IDE To build an example sketch in the Arduino IDE, select an example from the `File` > `Examples` > `JLed` menu. @@ -670,8 +695,8 @@ the host-based provided unit tests [is provided here](test/README.md). * add code * add [unit test(s)](test/) * add [documentation](README.md) -* make sure the cpp [linter](https://github.com/cpplint/cpplint) does not - report any problems (run `make lint`). Hint: use `clang-format` with the +* make sure the cpp [linter](https://github.com/cpplint/cpplint) does not + report any problems (run `make lint`). Hint: use `clang-format` with the provided [settings](.clang-format) * commit changes * submit a PR diff --git a/devbox.json b/devbox.json index 5dfc81c..e24a8ed 100644 --- a/devbox.json +++ b/devbox.json @@ -1,13 +1,13 @@ { "packages": [ - "python@3.11", + "python@3.13", "lcov@1.16", "pipx", "cpplint@2.0.0" ], "shell": { - "init_hook": [ - "echo 'Welcome to devbox!' > /dev/null" + "init_hook": [ + ". $VENV_DIR/bin/activate" ], "scripts": { "test": [ diff --git a/devbox.lock b/devbox.lock index 14600c5..5e96396 100644 --- a/devbox.lock +++ b/devbox.lock @@ -89,24 +89,60 @@ "resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#pipx", "source": "nixpkg" }, - "python@3.11": { - "last_modified": "2024-03-22T11:26:23Z", - "plugin_version": "0.0.3", - "resolved": "github:NixOS/nixpkgs/a3ed7406349a9335cb4c2a71369b697cecd9d351#python3", + "python@3.13": { + "last_modified": "2024-11-28T07:51:56Z", + "plugin_version": "0.0.4", + "resolved": "github:NixOS/nixpkgs/226216574ada4c3ecefcbbec41f39ce4655f78ef#python313", "source": "devbox-search", - "version": "3.11.8", + "version": "3.13.0", "systems": { "aarch64-darwin": { - "store_path": "/nix/store/c05vbvkjxarxkws9zkwrcwrzlsx9nd68-python3-3.11.8" + "outputs": [ + { + "name": "out", + "path": "/nix/store/fbyrkq5n04a9hn5zs26vrmqjzdx73d4g-python3-3.13.0", + "default": true + } + ], + "store_path": "/nix/store/fbyrkq5n04a9hn5zs26vrmqjzdx73d4g-python3-3.13.0" }, "aarch64-linux": { - "store_path": "/nix/store/pxzzyri1wbq7kc7pain665g94afkl4ww-python3-3.11.8" + "outputs": [ + { + "name": "out", + "path": "/nix/store/jbz9fj3sp5c8bf0s6d0bkjjj9mslxsrc-python3-3.13.0", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/60jgy93wj50wwimmhm2p53pzaiap8ypm-python3-3.13.0-debug" + } + ], + "store_path": "/nix/store/jbz9fj3sp5c8bf0s6d0bkjjj9mslxsrc-python3-3.13.0" }, "x86_64-darwin": { - "store_path": "/nix/store/1zaap1xxxvw2ypsgh1mfxb3wzdd49873-python3-3.11.8" + "outputs": [ + { + "name": "out", + "path": "/nix/store/c7j1vxcdcqswsddm5m1n2n4z5zfhmbq2-python3-3.13.0", + "default": true + } + ], + "store_path": "/nix/store/c7j1vxcdcqswsddm5m1n2n4z5zfhmbq2-python3-3.13.0" }, "x86_64-linux": { - "store_path": "/nix/store/7wz6hm9i8wljz0hgwz1wqmn2zlbgavrq-python3-3.11.8" + "outputs": [ + { + "name": "out", + "path": "/nix/store/0b83hlniyfbpha92k2j0w93mxdalv8kb-python3-3.13.0", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/xzhxhqs8my0yvfi09aj1s9i1s9nrmpvg-python3-3.13.0-debug" + } + ], + "store_path": "/nix/store/0b83hlniyfbpha92k2j0w93mxdalv8kb-python3-3.13.0" } } } diff --git a/examples/last_brightness/last_brightness.ino b/examples/last_brightness/last_brightness.ino new file mode 100644 index 0000000..b5cb71a --- /dev/null +++ b/examples/last_brightness/last_brightness.ino @@ -0,0 +1,38 @@ +// Stops an effect when a button is pressed (and hold). When the button is +// released, the LED will fade to off with starting the brightness value it had +// when the effect was stopped. +// +// dependency: arduinogetstarted/ezButton@1.0.6 to control the button +// +// Copyright 2024 by Jan Delgado. All rights reserved. +// https://github.com/jandelgado/jled +// +#include // arduinogetstarted/ezButton@1.0.6 +#include + +constexpr auto LED_PIN = 16; +constexpr auto BUTTON_PIN = 18; + +auto button = ezButton(BUTTON_PIN); + +// start with a pulse effect +auto led = + JLed(LED_PIN).DelayBefore(1000).Breathe(2000).Forever().MinBrightness(25); + +void setup() {} + +void loop() { + static int16_t lastBrightness = 0; + + button.loop(); + led.Update(&lastBrightness); + + if (button.isPressed()) { + // when the button is pressed, stop the effect on led, but keep the LED + // on with it's current brightness ... + led.Stop(JLed::KEEP_CURRENT); + } else if (button.isReleased()) { + // when the button is released, fade from the last brightness to 0 + led = JLed(LED_PIN).Fade(lastBrightness, 0, 1000); + } +} diff --git a/library.json b/library.json index 52dc8a0..448ad55 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "JLed", - "version": "4.14", + "version": "4.15.0", "description": "An embedded library to control LEDs", "license": "MIT", "frameworks": ["espidf", "arduino", "mbed"], diff --git a/library.properties b/library.properties index a11d63b..f71cc0d 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=JLed -version=4.14 +version=4.15.0 author=Jan Delgado maintainer=Jan Delgado sentence=An Arduino library to control LEDs diff --git a/platformio.ini b/platformio.ini index eb98c62..86f7718 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,7 @@ ;default_envs = esp8266 default_envs = esp32 ;default_envs = sparkfun_samd21_dev_usb -;default_envs = sparkfun_samd21_dev_usb +;default_envs = raspberrypi_pico_w ; uncomment example to build src_dir = examples/hello @@ -28,6 +28,7 @@ src_dir = examples/hello ;src_dir = examples/fade_on ;src_dir = examples/fade_off ;src_dir = examples/simple_on +;src_dir = examples/last_brightness ;src_dir = examples/multiled ;src_dir = examples/multiled_mbed ;src_dir = examples/user_func @@ -36,29 +37,26 @@ src_dir = examples/hello ;src_dir = examples/pulse ;src_dir = examples/fade_from_to +[env] +build_flags = -Isrc +build_src_filter = +<../../src/> +<./> +;lib_deps = arduinogetstarted/ezButton@1.0.6 + [env:nanoatmega328] platform = atmelavr board = nanoatmega328 framework = arduino -build_flags = -Isrc -build_src_filter = +<../../src/> +<./> [env:nucleo_f401re_mbed] platform=ststm32 board = nucleo_f401re framework = mbed -build_flags = -Isrc -build_src_filter = +<../../src/> +<./> upload_protocol=stlink [env:nucleo_f401re] -# nucleo f401re arduino framework support only on master at the moment platform=ststm32 -;platform=https://github.com/platformio/platform-ststm32.git board = nucleo_f401re framework = arduino -build_flags = -Isrc -build_src_filter = +<../../src/> +<./> upload_protocol=stlink debug_speed=auto @@ -66,29 +64,29 @@ debug_speed=auto platform = espressif8266 board = nodemcuv2 framework = arduino -build_flags = -Isrc -build_src_filter = +<../../src/> +<./> [env:esp32] lib_ldf_mode = off platform = espressif32 board = esp32dev framework = arduino -build_flags = -Isrc -build_src_filter = +<../../src/> +<./> [env:sparkfun_samd21_dev_usb] platform = atmelsam framework = arduino board = sparkfun_samd21_dev_usb -build_flags = -Isrc -build_src_filter = +<../../src/> +<./> [env:nano33ble] platform=https://github.com/platformio/platform-nordicnrf52.git board = nano33ble framework = arduino -build_flags = -Isrc -build_src_filter = +<../../src/> +<./> upload_protocol=stlink +[env:raspberrypi_pico_w] +build_flags = ${env.build_flags} -D ARDUINO_RASPBERRY_PI_PICO_W +platform = https://github.com/maxgerhardt/platform-raspberrypi.git +board = rpipicow +framework = arduino +board_build.filesystem_size = 0.5m +board_build.core = earlephilhower +upload_protocol = picotool diff --git a/src/jled_base.cpp b/src/jled_base.cpp index 6e59531..62d0358 100644 --- a/src/jled_base.cpp +++ b/src/jled_base.cpp @@ -26,8 +26,7 @@ namespace jled { // pre-calculated fade-on function. This table samples the function // y(x) = exp(sin((t - period / 2.) * PI / period)) - 0.36787944) // * 108. -// at x={0,32,...,256}. In FadeOnFunc() we us linear interpolation -// to +// at x={0,32,...,256}. In FadeOnFunc() we us linear interpolation to // approximate the original function (so we do not need fp-ops). // fade-off and breath functions are all derived from fade-on, see // below. @@ -70,14 +69,21 @@ uint8_t rand8() { // scale8(0, f) == 0 for all f // scale8(x, 255) == x for all x uint8_t scale8(uint8_t val, uint8_t factor) { - return (static_cast(val)*static_cast(1 + factor)) >> 8; + return (static_cast(val)*static_cast(factor))/255; } // interpolate a byte (val) to the interval [a,b]. uint8_t lerp8by8(uint8_t val, uint8_t a, uint8_t b) { if (a == 0 && b == 255) return val; // optimize for most common case - uint8_t delta = b - a; + const uint8_t delta = b - a; return a + scale8(val, delta); } +// the inverse of lerp8by8: invlerp8by8(lerp8by8(x, a, b,), a, b,) = x +uint8_t invlerp8by8(uint8_t val, uint8_t a, uint8_t b) { + const uint16_t delta = b - a; + if (delta == 0) return 0; + return (static_cast(val-a)*255)/(delta); +} + }; // namespace jled diff --git a/src/jled_base.h b/src/jled_base.h index 60c492e..1575dc1 100644 --- a/src/jled_base.h +++ b/src/jled_base.h @@ -46,6 +46,12 @@ uint8_t rand8(); 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); +uint8_t invlerp8by8(uint8_t val, uint8_t a, uint8_t b); + +template +static constexpr T __max(T a, T b) { + return (a > b) ? a : b; +} // a function f(t,period,param) that calculates the LEDs brightness for a given // point in time and the given period. param is an optionally user provided @@ -107,15 +113,21 @@ class BreatheBrightnessEvaluator : public CloneableBrightnessEvaluator { uint16_t duration_fade_on_; uint16_t duration_on_; uint16_t duration_fade_off_; + uint8_t from_; + uint8_t to_; public: BreatheBrightnessEvaluator() = delete; explicit BreatheBrightnessEvaluator(uint16_t duration_fade_on, uint16_t duration_on, - uint16_t duration_fade_off) + uint16_t duration_fade_off, + uint8_t from = 0, + uint8_t to = kFullBrightness) : duration_fade_on_(duration_fade_on), duration_on_(duration_on), - duration_fade_off_(duration_fade_off) {} + duration_fade_off_(duration_fade_off), + from_(from), + to_(to) {} BrightnessEvaluator* clone(void* ptr) const override { return new (ptr) BreatheBrightnessEvaluator(*this); } @@ -123,17 +135,21 @@ class BreatheBrightnessEvaluator : public CloneableBrightnessEvaluator { return duration_fade_on_ + duration_on_ + duration_fade_off_; } uint8_t Eval(uint32_t t) const override { + uint8_t val = 0; if (t < duration_fade_on_) - return fadeon_func(t, duration_fade_on_); + val = fadeon_func(t, duration_fade_on_); else if (t < duration_fade_on_ + duration_on_) - return kFullBrightness; + val = kFullBrightness; else - return fadeon_func(Period() - t, duration_fade_off_); + val = fadeon_func(Period() - t, duration_fade_off_); + return lerp8by8(val, from_, to_); } uint16_t DurationFadeOn() const { return duration_fade_on_; } uint16_t DurationFadeOff() const { return duration_fade_off_; } uint16_t DurationOn() const { return duration_on_; } + uint8_t From() const { return from_; } + uint8_t To() const { return to_; } }; class CandleBrightnessEvaluator : public CloneableBrightnessEvaluator { @@ -182,12 +198,9 @@ class TJLed { // 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_); - } + uint8_t Eval(uint32_t t) const { return brightness_eval_->Eval(t); } - // Write val out to "hardware", reverting signal when active-low is set. + // Write val out to the "hardware", inverting signal when active-low is set. void Write(uint8_t val) { hal_.analogWrite(IsLowActive() ? kFullBrightness - val : val); } @@ -259,15 +272,19 @@ class TJLed { } // Fade LED on - B& FadeOn(uint16_t duration) { - return SetBrightnessEval(new ( - brightness_eval_buf_) BreatheBrightnessEvaluator(duration, 0, 0)); + B& FadeOn(uint16_t duration, uint8_t from = 0, + uint8_t to = kFullBrightness) { + return SetBrightnessEval( + new (brightness_eval_buf_) + BreatheBrightnessEvaluator(duration, 0, 0, from, to)); } // Fade LED off - acutally is just inverted version of FadeOn() - B& FadeOff(uint16_t duration) { - return SetBrightnessEval(new ( - brightness_eval_buf_) BreatheBrightnessEvaluator(0, 0, duration)); + B& FadeOff(uint16_t duration, uint8_t from = kFullBrightness, + uint8_t to = 0) { + return SetBrightnessEval( + new (brightness_eval_buf_) + BreatheBrightnessEvaluator(0, 0, duration, to, from)); } // Fade from "from" to "to" with period "duration". Sets up the breathe @@ -275,9 +292,9 @@ class TJLed { // 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); + return FadeOn(duration, from, to); } else { - return FadeOff(duration).MinBrightness(to).MaxBrightness(from); + return FadeOff(duration, from, to); } } @@ -376,7 +393,13 @@ class TJLed { // Returns current maximum brightness level. uint8_t MaxBrightness() const { return maxBrightness_; } - // update brightness of LED using the given brightness evaluator + // update brightness of LED using the given brightness evaluator and the + // current time. If the optional pLast pointer is set, then the actual + // brightness value (if an update happened), will be returned through + // the pointer. The value returned will be the calculated value after + // min- and max-brightness scaling was applied, which is the value that + // is written to the output. + // // (brightness) ________________ // on 255 | ¸-' // | ¸-' @@ -385,50 +408,60 @@ class TJLed { // |<-delay before->|<--period-->|<-delay after-> (time) // | func(t) | // |<- num_repetitions times -> - bool Update() { return Update(hal_.millis()); } + bool Update(int16_t* pLast = nullptr) { + return Update(hal_.millis(), pLast); + } - bool Update(uint32_t now) { + bool Update(uint32_t t, int16_t* pLast = nullptr) { if (state_ == ST_STOPPED || !brightness_eval_) return false; if (state_ == ST_INIT) { - time_start_ = now + delay_before_; + time_start_ = t + delay_before_; state_ = ST_RUNNING; } else { // no need to process updates twice during one time tick. - if (!timeChangedSinceLastUpdate(now)) return true; + if (!timeChangedSinceLastUpdate(t)) return true; } - trackLastUpdateTime(now); + trackLastUpdateTime(t); - if (static_cast(now - time_start_) < 0) return true; + if (static_cast(t - time_start_) < 0) return true; + + auto writeCur = [this](uint32_t t, int16_t* p) { + const auto val = lerp8by8(Eval(t), minBrightness_, maxBrightness_); + if (p) { + *p = val; + } + Write(val); + }; // t cycles in range [0..period+delay_after-1] const auto period = brightness_eval_->Period(); - const auto t = (now - time_start_) % (period + delay_after_); if (!IsForever()) { const auto time_end = time_start_ + static_cast(period + delay_after_) * - num_repetitions_ - 1; + num_repetitions_ - + 1; - if (static_cast(now - time_end) >= 0) { + if (static_cast(t - time_end) >= 0) { // make sure final value of t = (period-1) is set state_ = ST_STOPPED; - const auto val = Eval(period - 1); - Write(val); + writeCur(period - 1, pLast); return false; } } + t = (t - time_start_) % (period + delay_after_); if (t < period) { state_ = ST_RUNNING; - Write(Eval(t)); + writeCur(t, pLast); } else { if (state_ == ST_RUNNING) { // when in delay after phase, just call Write() // once at the beginning. state_ = ST_IN_DELAY_AFTER_PHASE; - Write(Eval(period - 1)); + writeCur(period - 1, pLast); } } return true; @@ -466,7 +499,11 @@ class TJLed { // this is where the BrightnessEvaluator object will be stored using // placment new. Set MAX_SIZE to class occupying most memory - static constexpr auto MAX_SIZE = sizeof(CandleBrightnessEvaluator); + static constexpr auto MAX_SIZE = + __max(sizeof(CandleBrightnessEvaluator), + __max(sizeof(BreatheBrightnessEvaluator), + __max(sizeof(ConstantBrightnessEvaluator), // NOLINT + sizeof(BlinkBrightnessEvaluator)))); alignas(alignof( CloneableBrightnessEvaluator)) char brightness_eval_buf_[MAX_SIZE]; diff --git a/test/test_jled.cpp b/test/test_jled.cpp index 855b57d..b8bea35 100644 --- a/test/test_jled.cpp +++ b/test/test_jled.cpp @@ -43,8 +43,8 @@ class MockBrightnessEvaluator : public BrightnessEvaluator { // expected result when a JLed object is updated: return value // of Update() and the current brightness -typedef std::pair UpdateResult; -typedef std::vector UpdateResults; +using UpdateResult = std::pair; +using UpdateResults = std::vector; // helper to check if a led evaluates to given sequence. TODO use a catch // matcher @@ -190,7 +190,7 @@ TEST_CASE("using Fade() configures BreatheBrightnessEvaluator", "[jled]") { static void test() { SECTION("fade with from < to") { TestableJLed jled(1); - jled.Fade(100, 200, 300); + jled.Fade(100, 200, 300); // from, to, duration REQUIRE(dynamic_cast( jled.brightness_eval_) != nullptr); auto eval = dynamic_cast( @@ -198,8 +198,8 @@ TEST_CASE("using Fade() configures BreatheBrightnessEvaluator", "[jled]") { CHECK(300 == eval->DurationFadeOn()); CHECK(0 == eval->DurationOn()); CHECK(0 == eval->DurationFadeOff()); - CHECK(100 == jled.MinBrightness()); - CHECK(200 == jled.MaxBrightness()); + CHECK(100 == static_cast(eval->From())); + CHECK(200 == static_cast(eval->To())); } SECTION("fade with from >= to") { TestableJLed jled(1); @@ -211,8 +211,8 @@ TEST_CASE("using Fade() configures BreatheBrightnessEvaluator", "[jled]") { CHECK(0 == eval->DurationFadeOn()); CHECK(0 == eval->DurationOn()); CHECK(300 == eval->DurationFadeOff()); - CHECK(100 == jled.MinBrightness()); - CHECK(200 == jled.MaxBrightness()); + CHECK(100 == static_cast(eval->From())); + CHECK(200 == static_cast(eval->To())); } } }; @@ -261,7 +261,6 @@ TEST_CASE( TEST_CASE("CandleBrightnessEvaluator simulated candle flickering", "[jled]") { auto eval = CandleBrightnessEvaluator(7, 15, 1000); CHECK(1000 == eval.Period()); - // TODO(jd) do further and better tests CHECK(eval.Eval(0) > 0); CHECK(eval.Eval(999) > 0); } @@ -293,13 +292,13 @@ TEST_CASE("Forever flag is set by call to Forever()", "[jled]") { CHECK(jled.IsForever()); } -TEST_CASE("dont evalute twice during one time tick", "[jled]") { +TEST_CASE("dont evaluate twice during one time tick", "[jled]") { auto eval = MockBrightnessEvaluator(ByteVec{0, 1, 2}); TestJLed jled = TestJLed(1).UserFunc(&eval); - jled.Update(0); + jled.Update(0, nullptr); CHECK(eval.Count() == 1); - jled.Update(0); + jled.Update(0, nullptr); CHECK(eval.Count() == 1); jled.Update(1); @@ -317,15 +316,43 @@ TEST_CASE("Handles millis overflow during effect", "[jled]") { CHECK(jled.IsRunning()); CHECK(jled.Hal().Value() > 0); // Set time after overflow, before effect ends - CHECK(jled.Update(time+50)); + CHECK(jled.Update(time + 50)); CHECK(jled.IsRunning()); CHECK(jled.Hal().Value() > 0); // Set time after effect ends - CHECK_FALSE(jled.Update(time+150)); + CHECK_FALSE(jled.Update(time + 150)); CHECK_FALSE(jled.IsRunning()); CHECK(0 == jled.Hal().Value()); } +TEST_CASE("Update returns last written value if requested", "[jled]") { + auto eval = MockBrightnessEvaluator(ByteVec{0, 10}); + int16_t lastVal = -1; + TestJLed jled = TestJLed(1).UserFunc(&eval); + + jled.Update(0, &lastVal); + CHECK(lastVal == 0); + + jled.Update(1, &lastVal); + CHECK(lastVal == 10); +} + +TEST_CASE("Update doesn't change last value ptr if not updated", "[jled]") { + auto eval = MockBrightnessEvaluator(ByteVec{0, 10}); + int16_t lastVal = -1; + TestJLed jled = TestJLed(1).UserFunc(&eval).DelayBefore(1); + + jled.Update(0, &lastVal); + CHECK(lastVal == -1); + + jled.Update(5, &lastVal); + CHECK(lastVal == 10); + + lastVal = -1; + jled.Update(5, &lastVal); + CHECK(lastVal == -1); +} + TEST_CASE("Stop() stops the effect", "[jled]") { auto eval = MockBrightnessEvaluator(ByteVec{255, 255, 255, 0}); TestJLed jled = TestJLed(10).UserFunc(&eval); @@ -342,10 +369,11 @@ TEST_CASE("default Stop() sets the brightness to minBrightness", "[jled]") { TestJLed jled = TestJLed(10).UserFunc(&eval).MinBrightness(50); jled.Update(); - REQUIRE(130 == jled.Hal().Value()); // 100 scaled to [50,255] - jled.Stop(); + REQUIRE(130 == + static_cast(jled.Hal().Value())); // 100 scaled to [50,255] - CHECK(50 == jled.Hal().Value()); + jled.Stop(); + CHECK(50 == static_cast(jled.Hal().Value())); } TEST_CASE("Stop(FULL_OFF) sets the brightness to 0", "[jled]") { @@ -353,10 +381,11 @@ TEST_CASE("Stop(FULL_OFF) sets the brightness to 0", "[jled]") { 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); + REQUIRE(130 == + static_cast(jled.Hal().Value())); // 100 scaled to [50,255] - CHECK(0 == jled.Hal().Value()); + jled.Stop(TestJLed::eStopMode::FULL_OFF); + CHECK(0 == static_cast(jled.Hal().Value())); } TEST_CASE("Stop(KEEP_CURRENT) keeps the last brightness level", "[jled]") { @@ -364,10 +393,11 @@ TEST_CASE("Stop(KEEP_CURRENT) keeps the last brightness level", "[jled]") { 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); + REQUIRE(130 == + static_cast(jled.Hal().Value())); // 100 scaled to [50,255] - CHECK(130 == jled.Hal().Value()); + jled.Stop(TestJLed::eStopMode::KEEP_CURRENT); + CHECK(130 == static_cast(jled.Hal().Value())); } TEST_CASE("LowActive() inverts signal", "[jled]") { @@ -376,7 +406,7 @@ TEST_CASE("LowActive() inverts signal", "[jled]") { CHECK(jled.IsLowActive()); - jled.Update(0); + jled.Update(0, nullptr); CHECK(255 == jled.Hal().Value()); jled.Update(1); @@ -442,7 +472,7 @@ TEST_CASE("Update returns true while updating, else false", "[jled]") { TestJLed jled = TestJLed(10).UserFunc(&eval); // Update returns FALSE on last step and beyond, else TRUE - CHECK(jled.Update(0)); + CHECK(jled.Update(0, nullptr)); // when effect is done, we expect still false to be returned CHECK_FALSE(jled.Update(1)); @@ -511,13 +541,14 @@ TEST_CASE( using TestJLed::TestJLed; static void test() { TestableJLed jled(1); - auto eval = MockBrightnessEvaluator(ByteVec{0, 128, 255}); jled.UserFunc(&eval).MinBrightness(100).MaxBrightness(200); - CHECK(100 == jled.Eval(0)); - CHECK(150 == jled.Eval(1)); - CHECK(200 == jled.Eval(2)); + jled.Update(0, nullptr); + CHECK(100 == jled.Hal().Value()); + + jled.Update(2, nullptr); + CHECK(200 == jled.Hal().Value()); } }; TestableJLed::test(); @@ -574,3 +605,11 @@ TEST_CASE("lerp8by8 interpolates a byte into the given interval", CHECK(255 == (int)(jled::lerp8by8(255, 100, 255))); CHECK(200 == (int)(jled::lerp8by8(255, 100, 200))); } + +TEST_CASE("invlerp8by8 is the inverse of lerp8by8", "[invlerp8by8]") { + CHECK(0 == (int)(jled::invlerp8by8(0, 0, 255))); + CHECK(255 == (int)(jled::invlerp8by8(255, 0, 255))); + + CHECK(0 == (int)(jled::invlerp8by8(100, 100, 200))); + CHECK(255 == (int)(jled::invlerp8by8(200, 100, 200))); +}