I'm working on an arduino project that will serve as a lighting controller for aquarium lights. The concept is that the user will enter a start time, a duration, and a maximum light level (in percent). From there, a parabolic function is created such that the x-axis represents the number of minutes past the start time. The y-axis represents the light level in percent.
The formula for the parabolic curve is: f(x) = -((kx^2)/h^2) + (2kx)/h
where (h,k) is the vertex.
The y-intercept is 0, and the x-intercepts are 0 and 2h.
I am certain that this formula is correct, as I have verified it both using WolframAlpha, and recreating the formula in Excel and graphing the function results (spreadsheet attached).
However, the results I get from the below code are incorrect. What am I missing? Any help is greatly appreciated!
This project uses a ZS-042 DS3231 RCT module to set the time and save it when the power is disconnected.
// DS3232RTC - Version: Latest
#include <DS3232RTC.h>
struct ChParams {
time_t StartTime;
byte DurHour;
byte DurMin;
byte Max;
};
byte OldMinutes;
ChParams Ch1 = {28800,14,0,60}; //28800 seconds = 8am Start Time
byte Ch1Lvl = 0;
void setup() {
Serial.begin(9600);
setSyncProvider(RTC.get); // the function to get the time from the RTC
if(timeStatus() != timeSet)
Serial.println("Unable to sync with the RTC");
else
Serial.println("RTC has set the system time");
OldMinutes = minute();
Ch1Lvl = UpdateChLvl(Ch1,now());
}
void loop() {
if (OldMinutes != minute()) {
time_t CurrTime = now();
//for debugging. Remove for final.
Serial.println("");
Serial.println("Curr Time: " + PrintDateTime(CurrTime,2));
Serial.println("");
Ch1Lvl = UpdateChLvl(Ch1,CurrTime);
OldMinutes = minute();
}
}
byte UpdateChLvl(ChParams c, time_t t) {
//the formula for the curve is:
// y = -((kx^2)/h^2) + (2kx)/h
//where (h,k) is the vertex
int ChMins;
int DurMins = (c.DurHour*60) + c.DurMin;
float h = DurMins/2; //this is the x value of the vertex
//the y value of the vertex is c.Max
double Level;
//this is the x value to feed into the formula
ChMins = (floor(elapsedSecsToday(t)/60)) - (floor(elapsedSecsToday(c.StartTime)/60));
Level = (static_cast< double >(-1) * (static_cast< double >(c.Max*ChMins*ChMins) / static_cast< double >(h*h))) + (static_cast< double >(2*c.Max*ChMins) / static_cast< double >(h));
if (Level < 0) {
Level = 0;
}
//for debugging. remove for final.
Serial.println("Ch Start: " + PrintDateTime(c.StartTime,2));
Serial.println("Ch Dur: " + String(c.DurHour) + "h" + String(c.DurMin) + "m (" + String(DurMins) + ")");
Serial.println("Ch Max: " + String(c.Max));
Serial.println("Mins Elapsed Today: " + String(floor(elapsedSecsToday(t)/60)));
Serial.println("Ch Start Minutes: " + String(floor(elapsedSecsToday(c.StartTime)/60)));
Serial.println("Minute Diff (x value): " + String(ChMins));
Serial.println("Y value of vertex (h): " + String(h));
Serial.println("Level (output): " + String(Level));
Serial.println("");
return(round(Level));
}
My guess is that static_cast is some kind of macro, but I don't know. If it's to convert -1 to a double, if for no other reason than documentation, I would change -1 to -1.0.
Thanks for all the replies. To answer a few questions:
Seriously? Why would you do that?
Because a programmer friend told me that sometimes when you do certain types of math, the data types can influence the result. I admit that I didn't do a lot of reading on it, but what I found seemed to indicate that he was correct, and therefore I made an effort to convert every element of my equation to a double. I can tell by your response that this was clearly not the right thing to do.
I tried to compile the code but the IDE returned this error below
That's my mistake. I tried to include only the relevant parts of my code, and I forgot to include the global declaration of Ch1Lvl. I will modify my original post to include the declaration.
Either or both of the above can cause integer overflow.
Pete, am I understanding correctly that the change you've made here (aside from removing the "static cast" portions is simply to add a ".0" to each integer in the formula?
Yes. The constants then become floating point instead of integer and that forces the remainder of that portion of the calculation be done as floating point which won't overflow.
tko1982:
I can tell by your response that this was clearly not the right thing to do.
This is a far better choice...
tko1982:
Pete, am I understanding correctly that the change you've made here (aside from removing the "static cast" portions is simply to add a ".0" to each integer in the formula?
Essentially, it boils down to using the correct datatype. -1 is an int which is not the correct datatype. -1.0 is a double which is the correct datatype.
Thanks... that makes perfect sense. I'm sure it goes without saying that I'm not super familiar with C++, and am relying on google to help me with syntax. I appreciate your willingness to explain it to me!
I would do the math almost directly as it was written:
byte UpdateChLvl(ChParams c, time_t t)
{
unsigned long DurMins = (c.DurHour * 60) + c.DurMin;
float h = DurMins / 2;
float k = c.Max;
// If the StartTime has not been reached, lights off.
if (t < c.StartTime)
return 0;
// If the interval is over, lights off
if (t > (c.StartTime + (DurMins * 60)))
return 0;
float x = (t - c.StartTime) / 60; // Minutes into the DurMins interval
// Calculate the function at time x (minutes since c.StartTime)
// The formula for the curve is:
// f(x) = -((k x^2) / h^2) + (2kx) / h
// h is half the total interval in minutes
// k is height of the vertex (maximum brightness, c.Max)
byte Level = -((k * x * x) / (h * h)) + (2 * k * x) / h;
return Level;
}
A typed constant is often a good choice...
const double MinusOne = -1;
A better choice, IMHO, would be:
const double MinusOne = -1.0;
Make the initializer match the type being initialized.
Yes, I know that the compiler will do an implicit cast, to make -1 into -1.0, but why not make it clear that you are paying attention to what you are initializing?
I actually did that on purpose to demonstrate that (within reasonable limits) no matter what is on the right it becomes the desired datatype on the left.