Beer fermentation: While loop linked to Unix timestamp delta not working

Hello all,

I put together a circuit to control the fermentation of beer, using a cooling and heating device. This works fine, hence I will not go into its detail (and the posted code hence excludes this part of the algorithm).

The controller is expected to change temperature as time progresses, over a period of several days/weeks. As there are frequent power cuts where I live, I need to add a feature, allowing the algorithm to resume at the last set target temperature before loosing power.

The logic:

a) trefx: An absolute Unix timestamp reference I introduce into the code and upload to the Arduino Uno just before starting a fermentation cycle (I have a separate code to read the board’s Unixtime, hence making sure this reference is in sync with the board).
b) temp_table[15]: An array string with the target temperatures in Celsius.
c) time_step[15]: An array with the time steps expressed in hours.
d) fztemp: Target temperature used for maintaining/changing temperature.

When the algorithm starts it reads the current board’s Unixtime and compares is to the adjusted Unix time reference, using the the hours of the current position of the temp_step[] array. It then should change the target temp fztemp to the value in related position in the temp_table[15] array.

The problem:

The algorithm either sticks to 16 ºC or goes down straight to 2 ºC when I increase the difference between trefx and the current Unix time. After several searches and trial and error attempts I reached the end of my beginners wisdom.

The offending code:

#include "RTClib.h"
#include <SPI.h>

DateTime now;
RTC_DS1307 rtc;


long trefx = 1504951697;                                                            // Custom Unix time reference, added at the beginning of each fermentation cycle
int temp_table[15] = {16,15,14,13,12,11,10,9,8,7,6,5,4,3,2};                        // Target temperature in Celsius to be used 
int time_step[15] = {12,24,36,48,60,72,84,96,108,120,132,144,156,168,180};          // Time steps in hours
const int array_steps = 15;                                                         // Array length
int n = 0;

int fztemp = temp_table[0];                                                         // Setting the first target temp

void setup()
{  
  
  rtc.begin();
  

  // Target temp recovery loop based on absolute Unix time reference
  now = rtc.now();
  while (now.unixtime() >= trefx + (time_step[n] * 3600) and n < array_steps) {
    fztemp = temp_table[n];  
    ++n;
  } 
  // End target temp recovery loop
}



void loop()
{ 

   // Extensive code controlling the heating and cooling removed to avoid visual clutter
 
}

I need to add a feature, allowing the algorithm to resume at the last set target temperature before loosing power.

How, after the power is restored, is the Arduino supposed to know what “the last set target temperature” was?

long trefx = 1504951697;

Can time run backwards in your universe? Time is stored in unsigned longs, in my universe, because time can’t run backwards.

int temp_table[15] = {16,15,14,13,12,11,10,9,8,7,6,5,4,3,2};                        // Target temperature in Celsius to be used
int time_step[15] = {12,24,36,48,60,72,84,96,108,120,132,144,156,168,180};

Int arrays to store byte values? Why?

time_step[n] * 3600

12 * 3600 is NOT an integer value.

time_step[n] * 3600UL

would use an unsigned long to hold the intermediate result, meaning that no overflow occurs.

  while (now.unixtime() >= trefx + (time_step[n] * 3600) and n < array_steps) {

This whole statement needs more parentheses to make sure that the evaluation is done in the way that you expect.

12 * 3600 is NOT an integer value.

I'm pretty certain it is. It isn't a sixteen bit int value though.

AWOL: I'm pretty certain it is. It isn't a sixteen bit int value though.

Right. A little pedanticism never hurts.

Maybe I should have mentioned that I'm more in the Beer business, than the programming one. This is part of a personal project and all my learning I had to do on my own. This is the first time I actually ask for help. Hence, if you want to be pedantic, I will be able to offer you enough material for it.

As for the variable declaration, indeed, I should change the int to bytes (but the temperature array will likely be floats at a later stage, as I need decimals). Also, I unsigned the longs.

How, after the power is restored, is the Arduino supposed to know what "the last set target temperature" was?

True, this is not possible and sentence is poorly phrased. The code needs to set the temperature to what it normally would be at that stage of the time/temp fermentation profile. For this the elapsed time must be calculated using the manually introduced Unix timestamp.

So, taking the general indications you gave, plus some persistence, the code is now doing exactly that.

Thanks, M!

PaulS: Right. A little pedanticism never hurts.

someone could even say it's an integral value not an integer value, right? :grin:

but the temperature array will likely be floats at a later stage, as I need decimals

Why? Do you have a temperature sensor capable of less than 1 degree resolution? Does a few 10ths of a degree REALLY matter?

True, this is not possible

Well, it IS, if you store the target temperature in EEPROM each time the target changes.

Margoye: This is the first time I actually ask for help. Hence, if you want to be pedantic, I will be able to offer you enough material for it.

Don't worry about this and don't take any of this personally - it's just part of the fun here. You have the right attitude and willing to learn and build. The community will help

From a code perspective

int temp_table[15] = {16,15,14,13,12,11,10,9,8,7,6,5,4,3,2};                        // Target temperature in Celsius to be used 
int time_step[15] = {12,24,36,48,60,72,84,96,108,120,132,144,156,168,180};          // Time steps in hours
const int array_steps = 15;

best is to avoid literal numeric values so this would be better written as

const int array_steps = 15;
int temp_table[array_steps] = {16,15,14,13,12,11,10,9,8,7,6,5,4,3,2};                        // Target temperature in Celsius to be used 
int time_step[array_steps] = {12,24,36,48,60,72,84,96,108,120,132,144,156,168,180};          // Time steps in hours

so that you don't have to change 15 three times if you want to add a step.

but you can do better, the compiler does stuff for you and can decide the size based on the number of data you provide and can calculate that size. so the code could be written as

int temp_table[] = {16,15,14,13,12,11,10,9,8,7,6,5,4,3,2};                        // Target temperature in Celsius to be used 
int time_step[] = {12,24,36,48,60,72,84,96,108,120,132,144,156,168,180};          // Time steps in hours
const int array_steps = sizeof(temp_table)/sizeof(temp_table[0]); // the number of entries

Then you can worry about memory usage. Your numbers are small (less than 255, always positive), so no need to store them as int which will take 2 bytes in memory. The arrays won't change, so they could be const too. You can use only 1 byte for storage and thus write this as

const byte temp_table[] = {16,15,14,13,12,11,10,9,8,7,6,5,4,3,2};                        // Target temperature in Celsius to be used 
const byte time_step[] = {12,24,36,48,60,72,84,96,108,120,132,144,156,168,180};          // Time steps in hours
const byte array_steps = sizeof(temp_table)/sizeof(temp_table[0]); // the number of entries

and you just save 15+15+1 = 31 bytes of RAM! (the compiler will actually even optimize out array_steps as this can be fed directly inline in the assembly instead of fetching memory)

Then - Looking at your initial code - do I get it right that what you mean is

take beer to 16°, keep it there for 12 hours. take beer to 15°, keep it there for 24 hours (or is it the delta = keep it there for 12 hours?) take beer to 14°, keep it there for 36 hours (or is it the delta = keep it there for 12 hours?) take beer to 13°, keep it there for 48 hours (or is it the delta = keep it there for 12 hours?) take beer to 12°, keep it there for 60 hours (or is it the delta = keep it there for 12 hours?) ...

seems to me that you don't need the arrays at all for this right? the algorithm would be:

Start target T° at 17° Repeat drop target T° by 1° for 12 hours (or some arithmetic progression in time 12, 24, 36) maintain temperature at target T° until you reach T° == 2°


You mention frequent power outages - what does it mean for the process from a "beer quality" perspective and process? (I don't know anything about brewing beer). Do you need to restart from scratch depending on how long the outage was (like if the beer totally changed t°) or keep proceeding etc? probably need a better description of the whole process taking into account the failure so that this can be injected into the algorithm to make it fail safe.

J-M-L: You mention frequent power outages - what does it mean for the process from a "beer quality" perspective and process?

It can't be good.

OP: If the setup doesn't draw too much power and the outages don't last too long then maybe you should think about backing up the whole shebang with an Uninterruptible Power Supply.

PaulS: Why? Do you have a temperature sensor capable of less than 1 degree resolution? Does a few 10ths of a degree REALLY matter? Well, it IS, if you store the target temperature in EEPROM each time the target changes.

In general terms, you can get away with 1 degree resolution in Celsius, as yeast as about a 4-5ºC range for good fermentation. Nonetheless, within the range of those few degrees, yeast will produce surprisingly different flavors. Introducing 0.5ºC steps will not make or break the beer, but it provides a good tool for fine tuning the flavors. I could get away changing the scale to Fahrenheit as this would increase the resolution, but it would also mean having to convert temps manually when setting up the fermentation profile, increasing the chance for user error. Yes, the temp probe does provide the necessary resolution.

I will look more into the EEPROM memory to see if this is something I should take advantage of for this application.

@J-M-L

and you just save 15+15+1 = 31 bytes of RAM! (the compiler will actually even optimize out array_steps as this can be fed directly inline in the assembly instead of fetching memory)

Excellent recommendations, all now incorporated. Thanks for the step-by-step explanation.

Then - Looking at your initial code - do I get it right that what you mean is

take beer to 16°, keep it there for 12 hours.
take beer to 15°, keep it there for 24 hours (or is it the delta = keep it there for 12 hours?)
take beer to 14°, keep it there for 36 hours (or is it the delta = keep it there for 12 hours?)
take beer to 13°, keep it there for 48 hours (or is it the delta = keep it there for 12 hours?)
take beer to 12°, keep it there for 60 hours (or is it the delta = keep it there for 12 hours?)
...

seems to me that you don't need the arrays at all for this right?

The hours refer to the time elapsed since the start of the fermentation (meaning, since the Unix timestamp stored in trefx), overcoming the problem of not knowing the last set temp (given the value is not stored in the EEPROM memory). The time/temp array is set up for testing purposes only and doesn't always follow an arithmetic progression. Two examples of an actual typical fermentation profile are:

Example 1: Day 1: Hold temp at 9 ºC Day 12: Increase to and hold at 14ºC Day 15-27: Decrease temp by 0.5 every 12 hours (or with the current array type 1ºC every 24 hours) Day 28 onwards: Hold temp at 2ºC (here it stays for 1-2 months, before bottling).

Example 2: Day 1: Hold temp at 16ºC Day 4: Increase to and hold temp at 20ºC Day 7: Decrease to and hold temp at 18ºC Day 12: Decrease to and hold temp at 2ºC Day 14: Beer is bottled.

So the first example is a combination of a few temp/time points, followed by an arithmetic progression, while the second is just 4 temp/time points.

Nonetheless, I agree with you, that certainly I should be able to think of a way to make the profile setting more efficient, without making it overly complex, allowing for an easy profile setup for each beer.

OP:
If the setup doesn't draw too much power and the outages don't last too long then maybe you should think about backing up the whole shebang with an Uninterruptible Power Supply.

Upon power outage the Arduino draws energy from a 2000mAh battery. An LCD screen only powers up using a momentary push button, as to reduce power consumption. I might introduce a delay int the whole code, reducing the number of times information is requested from the 3 temperature sensors. There are also 3 relays, which continue drawing energy from the circuit. The usual power outage last just a couple of hours, but recently we had one which lasted 8 and drained the battery. I thought about connecting an UPS instead of the 2000 mAh battery, but since the heating/cooling devices of the fermenter are off anyway, this doesn't seem as a good use of resources. The fermenter is pretty well isolated and can hold temperatures pretty well. In the future I will possibly invest in a self-starting generator, which will then be able to sustain the fermentors as well.

Margoye: In the future I will possibly invest in a self-starting generator, which will then be able to sustain the fermentors as well.

Or, move your operation to a first-world country with more reliable power mains :smiling_imp:

Margoye:
The hours refer to the time elapsed since the start of the fermentation (meaning, since the Unix timestamp stored in trefx), overcoming the problem of not knowing the last set temp (given the value is not stored in the EEPROM memory). The time/temp array is set up for testing purposes only and doesn’t always follow an arithmetic progression. Two examples of an actual typical fermentation profile are:

OK so I would agree the array makes sense and gives you flexibility. I would use an array of struct to describe the steps (changing a bit what you have in mind for timing).

something like this for example

const uint32_t oneHour    = 3600ul;
const uint32_t oneDay     = 24 * oneHour;
const uint32_t oneWeek    = 7 * oneDay;

struct brewingStep {
  unsigned long durationSecond;
  byte startTemperature;
  byte endTemperature;
};

brewingStep lightBlondFruity[] = {
  {oneDay, 16, 16},         // hold at 16° for One day
  {3 * oneDay, 16, 20},     // then increase  T° from 16° to 20° over 3 days
  {oneDay, 20, 18},         // then decreease T° from 20° to 18° over 1 day
  {5 * oneDay, 18, 2 },     // then decrease  T° from 16° to 2°  over 5 days
  {4 * oneWeek, 2, 2 }      // then hold at   T° 2° for 4 weeks
};


const byte nbSteps = sizeof(lightBlondFruity) / sizeof(lightBlondFruity[0]);

void setup() {
  Serial.begin(115200);
  Serial.print("There are "); Serial.print(nbSteps); Serial.println(" steps for this beer");
  for (byte i = 0; i < nbSteps; i++) {
    Serial.print("Step #"); Serial.print(i + 1);Serial.print(": ");
    if (lightBlondFruity[i].startTemperature == lightBlondFruity[i].endTemperature) {
      Serial.print("Keep at "); Serial.print(lightBlondFruity[i].startTemperature); Serial.print(" degrees");
      Serial.print(" for "); Serial.print(lightBlondFruity[i].durationSecond); Serial.println(" seconds");
    } else {
      Serial.print("Bring from "); Serial.print(lightBlondFruity[i].startTemperature);
      Serial.print(" to "); Serial.print(lightBlondFruity[i].endTemperature); Serial.print(" degrees");
      Serial.print(" over "); Serial.print(lightBlondFruity[i].durationSecond); Serial.println(" seconds");
    }
  }
}

void loop() {}

will print out

</sub> <sub>There are 5 steps for this beer Step #1: Keep at 16 degrees for 86400 seconds Step #2: Bring from 16 to 20 degrees over 259200 seconds Step #3: Bring from 20 to 18 degrees over 86400 seconds Step #4: Bring from 18 to 2 degrees over 432000 seconds Step #5: Keep at 2 degrees for 2419200 seconds</sub> <sub>

In order to deal with power outage, I would write the UNIX start time indeed in seconds of the brewing in EEPROM memory as well as a flag (0x00 or 0xFF for example) to say if a brewing is running or not (set it to 0xFF when you start, turn in back to 0x00 when you are done brewing this batch)

When you start your arduino, your setup() would read the flag and if a brewing was running it means you got a power outage. I would load the start time and compare to current time and use that to find at what step of the array you currently are and keep controlling the T°. if the power outage has been short enough and you have a well insulated brewing chamber, then temperature probably did not change much anyway.

Seems not too complicated. (regulating the T° efficiently is a different topic probably)

You can also storing the array in flash memory (read about PROGMEM) to free up ram if needed, given the steps won’t change anyway

(Note that EEPROM has a limited number of 100,000 write cycles before the EEPROM starts to wear out. In your case, even if you write at the same location you are safe for 100.000 brewing cycles and, if the process is half a month long your EEPROM theoretically will start falling in more than 4000 years, so I guess you don’t mind too much :slight_smile: )

Hey J-M-L, that is a very interesting approach. You deserve a couple of beers for that! I'll go ahead and play with that. Thanks very much.