Hi Everyone,
I am creating a puzzle box and have several games where I would like an LED to fade to a different color without slowing down the other functions of the loop much. To give you an idea of the scope I have 6 mA gauges, 6 pots, 6 LEDs. When you move a pot the input is scaled to a PWM signal to the gauges when the gauge is in the right position the LED changes from red to green.
There are some rules on the pots so some additional calculations so when you move pot 2 it changes the output for gauges 1-3. When all the positions are correct the gauges do a synchronized wipe.
So all of that is working great. But what I can not figure out is how to get the leds to quickly fade from red to green and back again rather than a harsh switch. I am using fast.led lib but I am not married to it just the one I am most familiar with. I would want it to start the fade but fade back as soon as the pot is out of range. So if the pot remains in the same position after say 3 seconds its green, if the output is moved within that time or after fades back to red from whatever color it was in. This gives me 2 things I do not know how to do the biggest of which is how to do a neopixel fade and keep the main loop moving fast enough where the pots are responsive. I have a delay 50 statement in the code now and this is okay. I think I can figure out how to capture the state of the LED so I can make it smooth in both directions but the complexity starts to ramp up. Figuring 6 LEDs so I could use const int LEDState[18]. I have read several posts about doing something similar but each has not actually said it works and there are a litany of comments saying its a bad way to do it. Nested loops is the most common thought but the typical repose is people poking it with a stick like its poo.
I can provide my code so far but its messy and still in debug form. And its extra messy as I am very new to this!
Before I get wrapped around the axel what would you suggest?
you could introduce a timer based on the example of "BlinkWithoutDelay" and blend from one color to another.
blend is a function from the fastled lib.
you hand over two colors and a gradiant from 0..255 and the function returns a new color.
this is just a short example, you can blend pixel 1 by entering r g b into serial monitor.
the Serial outputs should give you some idea what is happening in the background.
// blend a pixel from one color to another
// https://forum.arduino.cc/t/fast-led-lib-fade-without-slowing-the-loop/869904
// by noiasca
#include <FastLED.h>
const uint16_t numLeds = 6;
const byte dataPin = 12;
const byte smoothInterval = 10;
CRGB leds[numLeds];
struct Timer {
CRGB targetColor;
uint32_t previousMillis;
byte counter = 0;
bool isBlending = false;
} timer[numLeds];
void update(byte currentLed)
{
uint32_t currentMillis = millis();
if (millis() - timer[currentLed].previousMillis > smoothInterval)
{
timer[currentLed].previousMillis = currentMillis;
if (timer[currentLed].counter < 255 && timer[currentLed].isBlending == true)
{
timer[currentLed].counter++;
CRGB tempColor = blend( leds[currentLed], timer[currentLed].targetColor, timer[currentLed].counter );
Serial.print(timer[currentLed].counter);
Serial.print("\ttempColor:"); Serial.print(tempColor.r); Serial.print(" "); Serial.print(tempColor.g); Serial.print(" "); Serial.println(tempColor.b);
leds[currentLed] = tempColor;
FastLED.show(); // optional
if (leds[currentLed] == timer[currentLed].targetColor)
{
Serial.print("target reached LED "); Serial.println(currentLed);
timer[currentLed].counter = 0;
timer[currentLed].isBlending = false;
}
}
}
}
void runSerial() // reads serial and activates a color blending on pixel 1
{
if (Serial.available())
{
char c = Serial.read();
switch (c)
{
case 'r':
timer[1].targetColor = CRGB::Red; timer[1].isBlending = true;
break;
case 'g':
timer[1].targetColor = CRGB::Green; timer[1].isBlending = true;
break;
case 'b':
timer[1].targetColor = CRGB::Blue; timer[1].isBlending = true;
break;
}
}
}
// This function sets up the ledsand tells the controller about them
void setup() {
Serial.begin(115200);
Serial.println(F("blend"));
FastLED.addLeds<WS2811, dataPin, RGB>(leds, numLeds);
leds[1] = CRGB::Green; // just a start color
timer[1].targetColor = CRGB::Blue; timer[1].isBlending = true; // you need to hand over a new color and start the blending
}
void loop() {
for (uint16_t i = 0; i<numLeds; i++) update(i); // call update for each LED
FastLED.show(); // call show once
runSerial(); // check if something is on serial
}
a full OOP way is for sure a better solution, but this simple struct ist at least a start
Thank you! And sorry I did not specify the LED type it’s just a bunch of neopixels. I can work around getting them into the right chip etc.
And I did check the link provided above and there was never an agreement on the best way to do this. Also it did not account for a loop function to do other operations.
I need to read and understand the above code it’s slightly above my pay grade at the moment.
Do you understand the example "BlinkWithoutDelay"? If not, what are you missing exactly?
Have you tested the sketch in #5 with your hardware?
What line of code is unclear after asking google?
Noiasca I understand blink without delay but this is different. Inherently to get a fade a transition over time is needed. If the condition changes then the transition needs to switch direction from its current location. All the code I have understood so far uses a small loop in the main loop to go from say 0-500 then it exits and continues on. While in the loop the inputs are not scanned.
The code I was referring too is above from someone else. They were nice enough to outline a method to do this. This is what I need to take some time to digest.
Thanks!
To do what you describe you need to take advantage of the fact that loop() does what it says, ie it loops
Save the value of millis() as the start time of the fade step and set the condition of the LED to the start value. Each time round loop() check whether the current time minus the start time equals or is greater than the period that the LEDs should stay in their current state. If not, then go round loop() until that period elapses, then change the LED state, save the start time again. Carry on doing this until the fade is complete
Because loop() is not blocked you can read an input each time round and change variables etc as a result. The obvious variable to change to reverse a fade would be the fade direction
The technique to do this is using a state-machine that is called repeatedly
Each time the function with the state-machine is called it executes one little step of whatever.
As the state-machine is controlled by a state-variable and each run of the function does only a small step for example increasing fading towards green and then the function is left
outside this function you can check for A-Z different other things that manipulate the state-variable which can include change fading from red to green to
the opposite
or fade-change to blue
or fade-change to yellow
or start beeping / blinking another LED in parallel to the fading or whatever you want
Noiasca sorry it was you that provided the code. Yes that looks like it will work very well for my application. Let’s me know my searches under the fast led lib are not good. Thanks for looking that up. Also let’s me know that you are really good at this. I am going to have to spend an hour or two running and modifying the script to understand it fully. This is a few levels past what I have done so far. The serial input is a nice addition, easy way to test the code without having the hardware hooked up.
My pleasure.
If you would like to go the next step, just let me know.
I've prepared an OOP variant, which makes the code cleaner, but I don't want to post the code as long as you don't feel confident with #5.
Please excuse the messy code, this is a hobby I started a few months ago with zero background or classes. My degree is biochem.
I mostly understand what you did with the code and have adapted crudely for my purposes. I need some time to be able to test this as I do not have everything wired at the moment.
When the update is called does this loop run independent of the main loop? Is this the standard way to handle things like this for example should I use your method to remove the polling of the inputs to a separate function outside of the main loop? The strut is really nice I did not know about this functionality till today. Your code made for a clean example of how that all works and that was a MAJOR gap in my understanding on how to track and use multiple variables for a given object. You all make this look so easy and I am hear to tell you its not at all lol.
Do you see any show stoppers in the below code. Again I am sorry this may make your skin crawl I am still working on formatting and getting things nice and clean. Just trying to use the concepts at the moment and get a functional program running.
Thank you.
//Gauge Puzzle May 30
// INCLUDES
#include <FastLED.h>
// CONSTANTS
#define NUM_LEDS 6
#define DATA_PIN 2
int brightness = 255;
CRGB leds[NUM_LEDS];
const byte meterPins[] = {3, 5, 6, 9, 10, 11};
const byte sliderPins[] = {A0, A1, A2, A3, A4, A5, A6, A7};
// Win condition const byte relayPin = 2;
const int targetValues[] = {20, 40, 65, 128, 128, 128, 128, 128};
const int tolerance = 4;
int y = 0;
const byte smoothInterval = 40;
struct Timer {
CRGB targetColor;
uint32_t previousMillis;
byte counter = 0;
bool isBlending = false;
}
timer[NUM_LEDS];
// GLOBALS
// 10-bit input values from the ADC have values in the range (0-1023)
int inputValues[8] = {};
// 8-bit output values to pass to AnalogWrite PWM output have values in the range (0-255)
int outputValues[6] = {};
bool isSolved = false;
void update(byte currentLed)
{
uint32_t currentMillis = millis();
if (millis() - timer[currentLed].previousMillis > smoothInterval)
{
timer[currentLed].previousMillis = currentMillis;
if (timer[currentLed].counter < 255 && timer[currentLed].isBlending == true)
{
timer[currentLed].counter++;
CRGB tempColor = blend( leds[currentLed], timer[currentLed].targetColor, timer[currentLed].counter );
Serial.print(timer[currentLed].counter);
Serial.print("\ttempColor:"); Serial.print(tempColor.r); Serial.print(" "); Serial.print(tempColor.g); Serial.print(" "); Serial.println(tempColor.b);
leds[currentLed] = tempColor;
FastLED.show(); // needed removed from loop
if (leds[currentLed] == timer[currentLed].targetColor)
{
Serial.print("target reached LED "); Serial.println(currentLed);
timer[currentLed].counter = 0;
timer[currentLed].isBlending = false;
}
}
}
}
void setup() {
Serial.begin(9600);
//Set up LEDs to start colors
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
for(int i=0; i<NUM_LEDS; i++){
leds[NUM_LEDS] = CRGB::Red;
FastLED.show();
}
// Initialize the output pins
for(int i=0; i<6; i++) {
pinMode(meterPins[i], OUTPUT);
}
}
void loop() {
for(int i=0; i<8; i++){
// Clear ADC
for(int x=0; x<2; x++) {
analogRead(sliderPins[i]);
delay(5);
}
inputValues[i] = analogRead(sliderPins[i]);
// Quick set for outputs
//outputValues[i] = inputValues[i];
// Testing Inputs, note floating pin produces variable results
//Serial.print(inputValues[i]);
//Serial.print(",");
//if(i>6){
//Serial.println("");
//}
}
//Setting up custom paramters/relationships
outputValues[0] = (inputValues[0] + inputValues[1]/ 5.5);
outputValues[1] = (inputValues[1] - inputValues[2]);
outputValues[2] = (inputValues[2]);
outputValues[3] = (inputValues[3]);
outputValues[4] = (inputValues[4]);
outputValues[5] = (inputValues[5]);
// Check outputs vs targets
bool allMetersCorrect = true;
// Loop over each output
for(int i=0; i<6; i++){
outputValues[i] = map(outputValues[i], 0, 1000, 0, 255);
outputValues[i] = constrain(outputValues[i], 0, 255);
analogWrite(meterPins[i], outputValues[i]);
if(abs(outputValues[i] - targetValues[i]) > tolerance) {
allMetersCorrect = false;
// Turn incorrect matches to red
timer[i].targetColor = CRGB::Red;
timer[i].isBlending = true;
}
else {
// Turn LED Green when input is correct
timer[i].targetColor = CRGB::Green;
timer[i].isBlending = true;
}
update(i);
}
if(allMetersCorrect && !isSolved) {
Serial.println("Solved!");
isSolved = true;
//Wipe gauges on solve need to add delay on return to stop gauge bounce
y = y+20;
if(y >= 850){
y=0;
}
for(int i=0; i<6; i++){
outputValues[i] = y;
}
}
// If the puzzle had been solved, but now the meters are no longer correct
else if(isSolved && !allMetersCorrect) {
Serial.println("Unsolved!");
isSolved = false;
}
//Display Inputs
//for(int i=0; i<8; i++) {
// Serial.print(inputValues[i]);
// Serial.print(",");
// if(i>6) { Serial.println(""); }
//}
//Display Outputs
for(int i=0; i<6; i++) {
Serial.print(outputValues[i]);
Serial.print(",");
if(i>4) {
Serial.println("");
}
}
delay(50);
}
I don't consider update(i) as independent from loop. update(i) is just like a small "sub program". The main reason to put that code in a function was to keep the time keeping things for the blending together. update(i) is still dependent on loop - it must be called as often as possible - at least in the interval lower than your parameter smoothInterval.
Yes, I consider this as good practice to have separate functions, to keep the loop short and clear and just call functions. You could use this for polling the inputs also. On the other hand, if you remove the commented Serial.prints only 4 lines of code remain and you have to decide if this is worth a separate function.
b)
I see delay() in your code.
The delay(5) after the analogRead will sum up to 5 * 2 * 6 = 60 ms, and the delay(50) at the end of loop is higher then the smoothInterval of 40. So you now have a "non-Blocking" blending update(), but you block the loop with delay() and therefore also black the blending update().
It might be, that currently you don't see negative effects, but don't add more delays but think about ways to get rid of your delays.
c)
you have following precompiler define:
#define NUM_LEDS 6
but you are not using it consequently. You have several code lines where you are using a hardcoded "6" - and not your "constant". This is called to use "magic numbers" - and that is considered as bad practice. By the way - consider to use a const byte (or another variable type) instead of these precompiler #define
Thank you.
To clarify when the subroutine update is called does the main loop stop till update is completed? I am confused if the program runs in a linear fashion or in parallel. I am deducing from your message its linear. So while its removed from the loop for simplicity either way the code will run the same way?
Totally understand on the delay statements will change those when everything is working.
I have no idea what define does vs the other ways to define a constant. I copied this from someone else code. However I can look this up and figure it out. I will also look for any hardcoded 6 and fix that just to get into the habit of doing it right. And what's wrong with a little magic!
The serial code did not work for me when I ran the program. Given I will probably not use this method much I did not trouble shoot it. So I removed this from my code as the triggers will be from inputs from pots.
As always thank you for all the help. And a quick sorry to all those reading this as this topic has some serious mission creep.
except for having two CPU-cores everything runs in serial but you can program in different ways:
As an allday analogon:
You can do eating a meal this way:
for or while loop
while (some rice is left on the plate)
put fork into rice
put fork into mouth
// stops when no more rice is on the plate
while (some meat is left on the plate)
put fork into meat
cut a piece of meat with knife
put fork into mouth
// stops when no more meat is on the plate
while (some limo is left in the glas)
take glas to the mouth
take a sip
put glas on the table
// stops when glas is empty
But that's not the way you eat
you are doing it like calling functions repeatedly
function some_rice
put fork into rice
put fork into mouth
function a_piece_of_meat
put fork into meat
cut a piece of meat with knife
put fork into mouth
function Take_a_sip
take glas to the mouth
take a sip
put glas on the table
loop
some_rice()
a_piece_of_meat()
Take_a_sip()
That is what you can do in coding too
repetated caling of functions
and the 100 times repeated call of a function does the same
as a for-loop
for (int I = 0; i< 100 i++)
only difference the 100 times looping is not done inside a for-loop the
looping is done by function loop itself
That's why it is called "loop"
best regards Stefan
the "program" (the loop) will call the function (in our example the function update)
"update" (the function) will do its job,
when "update" has finished
"loop" will continue with the next line.
That sounds misterious.
Have you chosen the right speed in your serial monitor?
Have you seen the output "blend" from the Serial.print in the setup in your serial monitor?
Serial.println(F("blend"));
have you entered r (or g or b) and have you pressed SEND in your serial monitor?
I did see the information blend in the serial
Monitor. The issue is when I type say r and hit send I see the arduino receive the input by the flash of the tx led but the function does not seem to be called. But like I said I don’t need that functionality so no need to put any time into it.