diff -r 000000000000 -r b74b0e4902c3 main/automation.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main/automation.c Sat Oct 20 13:23:15 2018 +0200 @@ -0,0 +1,1272 @@ +/** + * @file automation.c + * @brief Automation functions. + */ + +#include "config.h" + +int BoilPower = 100; +int LastMashStep = 0; +char temp_buf[64]; +char logline[128]; +char strftime_buf[64]; +bool loop; +bool CoolBeep = false; +bool Resume = false; +bool pumpRest = false; +bool updateRuntime = false; +bool NewMinute = false; +bool TempReached = false; +uint8_t MashState = MASH_NONE; +float temp_MLT; +float MinMash = 38.0; +float MaxMash = 80.0; +uint32_t power_MLT = 0; +uint32_t power_HLT = 0; +uint32_t counts = 0; +float stageTemp = 0.0; +uint16_t stageTime = 0; +uint16_t TimeWhirlPool = 0; +uint32_t TimeLeft = 0; +uint32_t TimeSpent = 0; +uint32_t SecsCount = 0; +uint32_t pumpTime = 0; +uint32_t TimeBrewing = 0; +uint16_t Steady = 0; +bool _NewMinute = false; +bool _UseHLT = false; +bool _Prompt = false; + +extern bool System_TimeOk; +extern sButton Buttons[MAXBUTTONS]; +extern int Main_Screen; +extern DS18B20_State *ds18b20_state; +extern DRIVER_State *driver_state; +extern SemaphoreHandle_t xSemaphoreDS18B20; +extern SemaphoreHandle_t xSemaphoreDriver; +extern double Output; +extern time_t now; +extern struct tm timeinfo; + +#ifdef CONFIG_TEMP_SENSORS_SIMULATOR +extern float Fake_MLT; +extern float Fake_HLT; +#endif + +static const char *TAG = "automation"; + + +/* + * Automation init function that only runs once when a + * new screen is entered. + */ +bool Automation_Init(void) +{ + switch (Main_Screen) { + case MAIN_AUTO_INIT: +#ifdef CONFIG_TEMP_SENSORS_SIMULATOR + Fake_MLT = recipe.MashStep[0].Temperature - 10; + Fake_HLT = recipe.SpargeTemp - 15; + if (xSemaphoreTake(xSemaphoreDS18B20, 10) == pdTRUE) { + ds18b20_state->mlt_temperature = ((int)(Fake_MLT * 16)) / 16.0; + ds18b20_state->hlt_temperature = ((int)(Fake_HLT * 16)) / 16.0; + xSemaphoreGive(xSemaphoreDS18B20); + } +#endif + for (int i = 0; i < 7; i++) { + if (recipe.MashStep[i].Resttime) + LastMashStep = i; + } + ESP_LOGI(TAG, "Last mash step %d", LastMashStep); + + // Check for a crashed session. + if (runtime.AutoModeStarted) { + TopMessage("Brouwen hervatten?"); + Buttons_Add( 40, 100, 80, 40, "Ja", 0); + Buttons_Add(200, 100, 80, 40, "Nee", 1); + Buttons_Show(); + SoundPlay(SOUND_Prompt); + loop = true; + while (loop) { + switch (Buttons_Scan()) { + case 0: loop = false; + Resume = true; + Main_Screen = runtime.StageResume; + TimeLeft = runtime.StageTimeLeft; + TimeBrewing = runtime.TimeBrewing; + _UseHLT = runtime.UseHLT; + MashState = MASH_NONE; + pumpTime = 0; + pumpRest = false; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->enable = true; + if (_UseHLT) { + driver_state->hlt_sp = recipe.SpargeTemp; + driver_state->hlt_mode = HLT_MODE_BANG; + } + xSemaphoreGive(xSemaphoreDriver); + } + ESP_LOGI(TAG, "Resume brew screen %d, time left %d", Main_Screen, TimeLeft); + log_begin((time_t)0); + update_json(); + log_annotation(ANNOTATION_SYSTEM, "Resume"); + return true; + break; + + case 1: loop = false; + Resume = false; + break; + + default: + break; + } + vTaskDelay(50 / portTICK_PERIOD_MS); + } + Buttons_Clear(); + TFT_fillScreen(_bg); + } + + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->enable = true; + xSemaphoreGive(xSemaphoreDriver); + } + runtime.AutoModeStarted = true; + runtime.UseHLT = _UseHLT = false; + runtime.TimeBrewing = 0; + TimeBrewing = 0; + runtime.StageResume = MAIN_AUTO_INIT; + runtime.StageTimeLeft = 0; + runtime.HopAddition = 0; + runtime.Logfile[0] = '\0'; + runtime.PumpCooling = false; + write_runtime(); + power_MLT = power_HLT = counts = 0; + log_clean(); + vTaskDelay(250 / portTICK_PERIOD_MS); // Allow some time + break; + + case MAIN_AUTO_DELAYSTART: + break; + + case MAIN_AUTO_HEATUP: + if (runtime.UseHLT) { + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->hlt_mode = HLT_MODE_BANG; + xSemaphoreGive(xSemaphoreDriver); + } + TopMessage("Spoelwater opwarmen"); + MLT_info(71, 26, false); + HLT_info(71,150, false, false); + } + break; + + case MAIN_AUTO_MASH_IN: + case MAIN_AUTO_MASH_1: + case MAIN_AUTO_MASH_2: + case MAIN_AUTO_MASH_3: + case MAIN_AUTO_MASH_4: + case MAIN_AUTO_MASH_5: + case MAIN_AUTO_MASH_6: + case MAIN_AUTO_MASH_OUT: + if (Main_Screen == MAIN_AUTO_MASH_IN) { + MinMash = 38.0; + MaxMash = recipe.MashStep[1].Temperature + 10.0; + TimeBrewing = 0; + runtime.TimeBrewing = 0; + if (System_TimeOk) { + time(&now); + localtime_r(&now, &timeinfo); + log_begin(now); + runtime.BrewStart = now; + } else { + log_begin((time_t)0); + runtime.BrewStart = (time_t)0; + } + updateRuntime = true; + } else if (Main_Screen == MAIN_AUTO_MASH_OUT) { + MinMash = 75.0; + MaxMash = 80.0; + } else { + MinMash = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN - 1].Temperature; + if (recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN + 1].Resttime) { + MaxMash = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN + 1].Temperature; + } else { + MaxMash = 75.0; + } + } + MashState = MASH_NONE; + pumpTime = 0; + pumpRest = false; + runtime.StageResume = Main_Screen; + updateRuntime = true; + TopMessage("Maischen"); + MLT_info(71, 26, false); + if (_UseHLT) { + HLT_info(71,170, false, true); + } + break; + + case MAIN_AUTO_TOBOIL: + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = stageTemp = config.BoilTemperature; + driver_state->mlt_mode = MLT_MODE_EXT; + driver_state->hlt_sp = 0.0; + driver_state->hlt_mode = HLT_MODE_NONE; + xSemaphoreGive(xSemaphoreDriver); + } + + runtime.StageResume = Main_Screen; + updateRuntime = true; + TempReached = false; + + TopMessage("Naar koken"); + MLT_info(71, 26, false); + Buttons_Add( 5, 30, 60, 40, "+sp", 0); + Buttons_Add(255, 30, 60, 40, "-sp", 1); + Buttons_Show(); + ESP_LOGI(TAG, "Mash done, going to boil."); + break; + + case MAIN_AUTO_BOILING: + if (Resume) { + TimeLeft = runtime.StageTimeLeft * 60; + } else { + // +1 minute for flameout and 2 seconds for a smooth transition. + runtime.StageTimeLeft = TimeLeft = (recipe.BoilTime * 60) + 60; + runtime.StageResume = Main_Screen; + runtime.HopAddition = 0; + } + SecsCount = 0; + + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = stageTemp = config.BoilTemperature; + driver_state->mlt_mode = MLT_MODE_EXT; + xSemaphoreGive(xSemaphoreDriver); + } + SoundPlay(SOUND_TempReached); + BoilPower = equipment.BoilPower; + updateRuntime = true; + TopMessage("Koken"); + MLT_info(71, 26, false); + Buttons_Add( 3, 30, 60, 40, "+sp", 0); + Buttons_Add(257, 30, 60, 40, "-sp", 1); + Buttons_Add( 3, 190, 60, 40, "+1m", 2); + Buttons_Add(257, 190, 60, 40, "-1m", 3); + Buttons_Show(); + ESP_LOGI(TAG, "Boil temperature reached, boil %d minutes", (TimeLeft / 60) -1); + log_annotation(ANNOTATION_STAGE, "Koken"); + break; + + case MAIN_AUTO_COOLING_H: + case MAIN_AUTO_COOLING_M: + case MAIN_AUTO_COOLING_C: + TempReached = false; + runtime.StageResume = Main_Screen; + runtime.StageTimeLeft = 0; + updateRuntime = true; + if ((Main_Screen == MAIN_AUTO_COOLING_H) && (! recipe.Whirlpool7)) { + // Skip cooling before whirlpool 74 degrees + Main_Screen = MAIN_AUTO_COOLING_M; + return true; //goto startover; + } + if ((Main_Screen == MAIN_AUTO_COOLING_M) && (! recipe.Whirlpool6)) { + // Skip cooling before whirlpool 63 degrees. + Main_Screen = MAIN_AUTO_COOLING_C; + return true; //goto startover; + } + TopMessage("Start koelen?"); + Buttons_Add( 40, 100, 80, 40, "Start", 0); + Buttons_Add(200, 100, 80, 40, "Stop", 1); + Buttons[1].dark = true; + Buttons_Show(); + SoundPlay(SOUND_Prompt); + _Prompt = true; + break; + + case MAIN_AUTO_WHIRLPOOL7: + case MAIN_AUTO_WHIRLPOOL6: + case MAIN_AUTO_WHIRLPOOL2: + TempReached = true; + runtime.StageResume = Main_Screen; + updateRuntime = true; + if ((Main_Screen == MAIN_AUTO_WHIRLPOOL9) && (! recipe.Whirlpool9)) { + // Skip whirlpool 93 degrees. + Main_Screen = MAIN_AUTO_COOLING_H; + return true; //goto startover; + } + if ((Main_Screen == MAIN_AUTO_WHIRLPOOL7) && (! recipe.Whirlpool7)) { + // Skip whirlpool 74 degrees. + Main_Screen = MAIN_AUTO_COOLING_M; + return true; //goto startover; + } + if ((Main_Screen == MAIN_AUTO_WHIRLPOOL6) && (! recipe.Whirlpool6)) { + // Skip whirlpool 63 degrees. + Main_Screen = MAIN_AUTO_COOLING_C; + return true; //goto startover; + } + if ((Main_Screen == MAIN_AUTO_WHIRLPOOL2) && (! recipe.Whirlpool2)) { + // Skip final whirlpool. + Main_Screen = MAIN_AUTO_DONE; + return true; //goto startover; + } + + TopMessage("Start Whirlpool?"); + Buttons_Add( 40, 100, 80, 40, "Start", 0); + Buttons_Add(200, 100, 80, 40, "Stop", 1); + Buttons[1].dark = true; + Buttons_Show(); + SoundPlay(SOUND_Prompt); + _Prompt = true; + break; + + case MAIN_AUTO_DONE: + case MAIN_AUTO_ABORT: + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->enable = false; + driver_state->mlt_mode = MLT_MODE_NONE; + driver_state->mlt_sp = 0.0; + driver_state->hlt_mode = HLT_MODE_NONE; + driver_state->hlt_sp = 0.0; + driver_state->pump_run = 0; + xSemaphoreGive(xSemaphoreDriver); + } + _fg = TFT_YELLOW; + TFT_setFont(DEJAVU24_FONT, NULL); + if (Main_Screen == MAIN_AUTO_DONE) { + TFT_print("Brouwen is gereed.", CENTER, CENTER); + ESP_LOGI(TAG, "Brew is done"); + SoundPlay(SOUND_End); + } else { + TFT_print("Brouwen is afgebroken.", CENTER, CENTER); + ESP_LOGI(TAG, "Brew is aborted"); + SoundPlay(SOUND_Warn); + } + log_close(); + runtime.Logfile[0] = '\0'; + runtime.BrewStart = (time_t)0; + runtime.AutoModeStarted = false; + runtime.StageResume = MAIN_MODE_FREE; + runtime.PumpCooling = false; + runtime.HopAddition = 0; + runtime.TimeBrewing = 0; + runtime.StageTimeLeft = 0; + updateRuntime = true; + Buttons_Add(130, 200, 60, 40, "Ok", 0); + Buttons[0].dark = true; + Buttons_Show(); + break; + + default: + break; + } + + return false; +} + + + +/* + * Automation loop screens. Mostly non-blocking. + */ +bool Automation_Loop(void) +{ + static bool beeped = false; + char tmp[32]; + uint16_t y; + + switch (Main_Screen) { + + case MAIN_AUTO_INIT: + /* + * Present selected equipment and recipe. + */ + read_recipe(config.RecipeRec); + y = 28; + TopMessage("Automaat"); + TFT_setFont(DEFAULT_FONT, NULL); + ShowText(2,y,"Installatie", equipment.Name); + y += 16; + ShowText(2,y,"Recept", recipe.Name); + y += 16; + ShowFloat(2, y, "Maisch in", " C", recipe.MashStep[0].Temperature, 2); + ShowFloat(162, y, "Spoelwater", " C", recipe.SpargeTemp, 2); + y += 16; + _fg = TFT_WHITE; + TFT_print("Maisch stap", 2, y); + TFT_print("Temp.", 200, y); + TFT_print("Rust", 260, y); + _fg = TFT_YELLOW; + y += 16; + for (int i = 1; i < 8; i++) { + if (recipe.MashStep[i].Resttime) { + TFT_print(recipe.MashStep[i].Name, 2, y); + sprintf(tmp, "%.2f", recipe.MashStep[i].Temperature); + TFT_print(tmp, 200, y); + sprintf(tmp, "%2d min", recipe.MashStep[i].Resttime); + TFT_print(tmp, 260, y); + y += 16; + } + } + ShowInteger(2, y, "Kooktijd", " miniuten", recipe.BoilTime); + y += 16; + if (recipe.Additions) { + _fg = TFT_YELLOW; + sprintf(tmp, "%d ", recipe.Additions); + TFT_print(tmp, 2, y); + _fg = TFT_WHITE; + TFT_print("toevoegingen om", LASTX, y); + _fg = TFT_YELLOW; + for (int i = 1; i <= recipe.Additions; i++) { + sprintf(tmp, " %d", recipe.Addition[i-1].Time); + TFT_print(tmp, LASTX, y); + } + _fg = TFT_WHITE; + TFT_print(" minuten", LASTX, y); + } else { + _fg = TFT_WHITE; + TFT_print("Geen hop toevoegingen.", 2, y); + } + y += 16; + ShowFloat(2, y, "Koelen tot", " C", recipe.CoolTemp, 2); + if (recipe.Whirlpool9) { + ShowInteger(2, y, "Whirlpool 88..100 graden", " minuten", recipe.Whirlpool9); + y += 16; + } + if (recipe.Whirlpool7) { + ShowInteger(2, y, "Whirlpool 71..77 graden", " minuten", recipe.Whirlpool7); + y += 16; + } + if (recipe.Whirlpool6) { + ShowInteger(2, y, "Whirlpool 60..66 graden", " minuten", recipe.Whirlpool6); + y += 16; + } + if (recipe.Whirlpool2) { + ShowInteger(2, y, "Whirlpool koud", " minuten", recipe.Whirlpool2); + y += 16; + } + Buttons_Add( 0, 210, 70, 30, "Stop" , 0); + Buttons_Add(250, 210, 70, 30, "Start" , 1); + Buttons[0].dark = true; + Buttons_Show(); + loop = true; + while (loop) { + switch (Buttons_Scan()) { + case 0: loop = false; + Main_Screen = MAIN_AUTO_ABORT; + break; + + case 1: loop = false; + break; + + default: break; + } + vTaskDelay(20 / portTICK_PERIOD_MS); + } + if (Main_Screen == MAIN_AUTO_ABORT) + break; + + _UseHLT = false; + _bg = TFT_BLACK; + TFT_fillScreen(_bg); + TopMessage("Maisch water aanwezig?"); + Buttons_Clear(); + Buttons_Add( 40, 100, 80, 40, "Ja", 0); + Buttons_Add(200, 100, 80, 40, "Nee", 1); + Buttons_Show(); + SoundPlay(SOUND_Prompt); + loop = true; + while (loop) { + switch (Buttons_Scan()) { + case 0: loop = false; + break; + + case 1: loop = false; + Main_Screen = MAIN_AUTO_ABORT; + break; + + default: break; + } + vTaskDelay(20 / portTICK_PERIOD_MS); + } + if (Main_Screen == MAIN_AUTO_ABORT) + break; + + if ((equipment.SSR2 == SSR2_HLT_SHARE) || (equipment.SSR2 == SSR2_HLT_IND)) { + TopMessage("Spoelwater aanwezig?"); + SoundPlay(SOUND_Prompt); + loop = true; + while (loop) { + switch (Buttons_Scan()) { + case 0: loop = false; + _UseHLT = true; + break; + + case 1: loop = false; + break; + + default: break; + } + vTaskDelay(20 / portTICK_PERIOD_MS); + } + runtime.UseHLT = _UseHLT; + } else { + runtime.UseHLT = _UseHLT = false; + } + updateRuntime = true; + if (_UseHLT) { + /* + * Calculate HLT setpoint for pre-heat. Substract the + * available Mash rest times, asume 0.5 degrees/minute + * heat capacity during mash. + */ + int AvailableTime = 0; + for (int i = 1; i < 6; i++) // Only normal Mash steps + AvailableTime += recipe.MashStep[i].Resttime; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->hlt_sp = recipe.SpargeTemp - ((AvailableTime / 2) + 2); + ESP_LOGI(TAG, "HLT preheat set to %4.1f", driver_state->hlt_sp); + xSemaphoreGive(xSemaphoreDriver); + } + } + Buttons_Clear(); + Main_Screen = MAIN_AUTO_DELAYSTART; + break; + + case MAIN_AUTO_DELAYSTART: + Main_Screen = MAIN_AUTO_HEATUP; + break; + + case MAIN_AUTO_HEATUP: + if (! runtime.UseHLT) { // Skip if HLT is off + Main_Screen = MAIN_AUTO_MASH_IN; + break; + } + + MLT_info(71, 26, true); + HLT_info(71,150, true, false); + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (driver_state->hlt_pv >= driver_state->hlt_sp) { + Main_Screen = MAIN_AUTO_MASH_IN; + driver_state->hlt_sp = recipe.SpargeTemp; // Set final setpoint + } + xSemaphoreGive(xSemaphoreDriver); + } + break; + + case MAIN_AUTO_MASH_IN: + case MAIN_AUTO_MASH_1: + case MAIN_AUTO_MASH_2: + case MAIN_AUTO_MASH_3: + case MAIN_AUTO_MASH_4: + case MAIN_AUTO_MASH_5: + case MAIN_AUTO_MASH_6: + case MAIN_AUTO_MASH_OUT: + temp_MLT = 0.0; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + temp_MLT = driver_state->mlt_pv; + + if (MashState == MASH_ADD || MashState == MASH_REMOVE) { + driver_state->pump_run = 0; + } else if (MashState != MASH_NONE) { + if (Main_Screen == MAIN_AUTO_MASH_IN) { + driver_state->pump_run = (equipment.PumpPreMash && ! pumpRest) ? 1 : 0; + } else if (Main_Screen == MAIN_AUTO_MASH_OUT) { + driver_state->pump_run = (equipment.PumpMashOut && ! pumpRest) ? 1 : 0; + } else { + driver_state->pump_run = (equipment.PumpOnMash && ! pumpRest) ? 1 : 0; + } + } + xSemaphoreGive(xSemaphoreDriver); + } + if (MashState == MASH_NONE) { + stageTemp = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Temperature; + stageTime = recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Resttime; + TempReached = false; + if (stageTime == 0) { + ESP_LOGI(TAG, "Mash step %d skipped", Main_Screen - MAIN_AUTO_MASH_IN); + Main_Screen++; + break; + } + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = stageTemp; + driver_state->mlt_mode = MLT_MODE_PID; + xSemaphoreGive(xSemaphoreDriver); + } + MashState = MASH_WAITTEMP; + ESP_LOGI(TAG, "Mash step %d time: %d temp: %4.1f min: %4.1f max: %4.1f", + Main_Screen - MAIN_AUTO_MASH_IN, stageTime, stageTemp, MinMash, MaxMash); + + if (Main_Screen > MAIN_AUTO_MASH_IN) { + // Do not annotate before the log is open. + if (Main_Screen == MAIN_AUTO_MASH_OUT) { + log_annotation(ANNOTATION_STAGE, "Uitmaischen"); + } else { + sprintf(logline, "Maisch: %d", Main_Screen - MAIN_AUTO_MASH_IN); + log_annotation(ANNOTATION_STAGE, logline); + } + } + + if (strlen(recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Name)) { + TopMessage(recipe.MashStep[Main_Screen - MAIN_AUTO_MASH_IN].Name); + } else { + sprintf(temp_buf, "Maisch stap #%d", Main_Screen - MAIN_AUTO_MASH_IN); + TopMessage(temp_buf); + } + Buttons_Add( 5, 30, 60, 40, "+sp", 0); + Buttons_Add(255, 30, 60, 40, "-sp", 1); + Buttons_Show(); + + } else if (MashState == MASH_WAITTEMP) { + pumpRest = false; + if (temp_MLT < stageTemp) { + Steady = 0; + } + if ((temp_MLT >= stageTemp) && (Steady > 10)) { + SoundPlay(SOUND_TempReached); + TempReached = true; + MashState = MASH_REST; + if (Main_Screen == MAIN_AUTO_MASH_IN) { + TimerSet(0); + } else { + if (Resume && (runtime.StageTimeLeft < stageTime)) + TimerSet(runtime.StageTimeLeft * 60); + else + TimerSet(stageTime * 60); + } + Resume = false; + runtime.StageTimeLeft = TimeLeft / 60; + updateRuntime = true; + ESP_LOGI(TAG, "Mash step %d temperature reached, rest time %d", Main_Screen - MAIN_AUTO_MASH_IN, TimeLeft / 60); + Buttons_Clear(); + Buttons_Add( 0, 120, 60, 40, "+1m", 0); + Buttons_Add(260, 120, 60, 40, "-1m", 1); + Buttons_Show(); + } + switch (Buttons_Scan()) { + case 0: if (stageTemp < MaxMash) { + stageTemp += 0.25; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = stageTemp; + xSemaphoreGive(xSemaphoreDriver); + } + } + break; + + case 1: if (stageTemp > MinMash) { + stageTemp -= 0.25; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = stageTemp; + xSemaphoreGive(xSemaphoreDriver); + } + } + break; + + default: + break; + } + if (NewMinute) + updateRuntime = true; + + } else if (MashState == MASH_REST) { + /* + * Mash step rest time and pump control + */ + if (((Main_Screen == MAIN_AUTO_MASH_OUT) && equipment.PumpMashOut) || + ((Main_Screen != MAIN_AUTO_MASH_OUT) && equipment.PumpOnMash)) { + float DeltaTemp = equipment.PumpRest * stageTemp / 120; // Maximum temperature drop before heating again. + if (pumpTime >= (equipment.PumpCycle + equipment.PumpRest) || ((stageTemp - temp_MLT) > DeltaTemp)) { + pumpTime = 0; + } + if (pumpTime >= equipment.PumpCycle) { + if (! pumpRest) { + pumpRest = true; + ESP_LOGI(TAG, "Pump rest"); + } + } else { + if (pumpRest) { + pumpRest = false; + ESP_LOGI(TAG, "Pump start"); + } + } + } + if (TimeLeft) { + switch (Buttons_Scan()) { + case 0: TimeLeft += 60; + runtime.StageTimeLeft = TimeLeft / 60; + updateRuntime = true; + break; + + case 1: if (TimeLeft < 60) + TimeLeft = 0; + else + TimeLeft -= 60; + runtime.StageTimeLeft = TimeLeft / 60; + updateRuntime = true; + break; + + default: break; + } + } + + if (TimeLeft == 0) { + runtime.StageTimeLeft = TimeLeft / 60; + updateRuntime = true; + if ((Main_Screen == MAIN_AUTO_MASH_IN) && config.AskAdd) { + /* + * Add Mash prompt. + */ + log_annotation(ANNOTATION_EVENT, "Mout storten"); + Buttons_Clear(); + Buttons_Add( 5,120, 60, 40, "Halt", 0); + Buttons[0].dark = true; + Buttons_Add(255,120, 60, 40, "Ok", 1); + Buttons_Show(); + _fg = TFT_WHITE; + _bg = TFT_BLACK; + TFT_setFont(DEJAVU24_FONT, NULL); + TFT_print("Mout storten?", CENTER, 135); + SoundPlay(SOUND_Prompt); + MashState = MASH_ADD; + ESP_LOGI(TAG, "Mash add prompt"); + break; + } + if (((Main_Screen - MAIN_AUTO_MASH_IN) == LastMashStep) && config.AskIodine) { + /* + * Iodone test prompt. + */ + log_annotation(ANNOTATION_EVENT, "Jodium test"); + TFT_fillRect(0, 120, 320, 50, TFT_BLACK); + Buttons_Clear(); + Buttons_Add( 5,120, 60, 40, "Halt", 0); + Buttons[0].dark = true; + Buttons_Add(255,120, 60, 40, "Ok", 1); + Buttons_Show(); + _fg = TFT_WHITE; + _bg = TFT_BLACK; + TFT_setFont(DEJAVU24_FONT, NULL); + TFT_print("Jodium test?", CENTER, 127); + SoundPlay(SOUND_Prompt); + beeped = false; + TimerSet(config.IodineTime * 60); + MashState = MASH_IODINE; + ESP_LOGI(TAG, "Mash iodine test prompt"); + break; + } + if ((Main_Screen == MAIN_AUTO_MASH_OUT) && config.AskRemove) { + /* + * Mash remove prompt. + */ + log_annotation(ANNOTATION_EVENT, "Mout verwijderen"); + TFT_fillRect(0, 120, 320, 50, TFT_BLACK); + Buttons_Clear(); + Buttons_Add( 5,120, 60, 40, "Halt", 0); + Buttons[0].dark = true; + Buttons_Add(255,120, 60, 40, "Ok", 1); + Buttons_Show(); + _fg = TFT_WHITE; + _bg = TFT_BLACK; + TFT_setFont(DEJAVU18_FONT, NULL); + TFT_print("Mout verwijderen?", CENTER, 135); + SoundPlay(SOUND_Prompt); + MashState = MASH_REMOVE; + ESP_LOGI(TAG, "Mash remove prompt"); + break; + } + if (Main_Screen != MAIN_AUTO_ABORT) + Main_Screen++; + TempReached = false; + } else { + TimerShow(TimeLeft, 65, 122); + if (NewMinute) { + runtime.StageTimeLeft = TimeLeft / 60; + updateRuntime = true; + } + } + } else if (MashState == MASH_ADD) { + switch (Buttons_Scan()) { + case 0: Main_Screen = MAIN_AUTO_ABORT; + break; + case 1: Main_Screen++; + break; + default: break; + } + } else if (MashState == MASH_IODINE) { + if (TimeSpent % 45 == 0) { + if (! beeped) { + SoundPlay(SOUND_Warn); + beeped = true; + } + } else { + beeped = false; + } + switch (Buttons_Scan()) { + case 0: Main_Screen = MAIN_AUTO_ABORT; + break; + case 1: Main_Screen++; + break; + default: break; + } + if (TimeLeft == 0) { + Main_Screen++; + } + } else if (MashState == MASH_REMOVE) { + switch (Buttons_Scan()) { + case 0: Main_Screen = MAIN_AUTO_ABORT; + break; + case 1: Main_Screen++; + break; + default: break; + } + } /* MashState */ + MLT_info(71, 26, true); + if (_UseHLT) { + HLT_info(71, 170, true, true); + } + break; + + case MAIN_AUTO_TOBOIL: + Output = 255; + /* + * Go to the boil temperature and wait until it is steady. + */ + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (driver_state->mlt_pv < stageTemp) { + Steady = 0; + } else { + if (Steady > 10) { + Main_Screen = MAIN_AUTO_BOILING; + TempReached = true; + } + } + driver_state->pump_run = (equipment.PumpOnBoil && (driver_state->mlt_pv < equipment.PumpMaxTemp)) ? 1 : 0; + xSemaphoreGive(xSemaphoreDriver); + } + + MLT_info(71, 26, true); + switch (Buttons_Scan()) { + case 0: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp += 0.25; + xSemaphoreGive(xSemaphoreDriver); + } + break; + + case 1: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp -= 0.25; + xSemaphoreGive(xSemaphoreDriver); + } + break; + + default: break; + } + if (Resume) + Resume = false; + break; + + case MAIN_AUTO_BOILING: + if (Resume) + Resume = false; + if (NewMinute) { + if ((runtime.HopAddition < recipe.Additions) && (TimeLeft <= ((recipe.Addition[runtime.HopAddition].Time * 60) + 60))) { + ESP_LOGI(TAG, "Hop addition %d at %d minutes", runtime.HopAddition + 1, recipe.Addition[runtime.HopAddition].Time); + TopMessage(recipe.Addition[runtime.HopAddition].Name); + sprintf(logline, "Hopgift %d %s", runtime.HopAddition + 1, recipe.Addition[runtime.HopAddition].Name); + log_annotation(ANNOTATION_EVENT, logline); + SoundPlay(SOUND_AddHop); + runtime.HopAddition++; + } else { + TopMessage("Koken"); + } + runtime.StageTimeLeft = TimeLeft / 60; + updateRuntime = true; + } + if (TimeLeft < 60) { + if (Output) { + log_annotation(ANNOTATION_STAGE, "Vlamuit"); + } + // Flameout + Output = 0; + } else if (driver_state->mlt_pv >= stageTemp) { + Output = (int)((BoilPower * 255.0) / 100.0); + if (Buttons[4].x == -1) { + Buttons_Add( 3,110, 60, 40, "+%", 4); + Buttons_Add(257,110, 60, 40, "-%", 5); + Buttons_Show(); + } + } else { + Output = 255; + if (Buttons[4].x != -1) { + Buttons[4].x = Buttons[5].x = -1; + Buttons_Show(); + TFT_fillRect( 3,110, 60, 40, TFT_BLACK); + TFT_fillRect(257,110, 60, 40, TFT_BLACK); + } + } + + MLT_info(71, 26, true); + TimerShow(TimeLeft, 65, 190); + + switch (Buttons_Scan()) { + case 0: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp += 0.25; + xSemaphoreGive(xSemaphoreDriver); + } + break; + + case 1: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp -= 0.25; + xSemaphoreGive(xSemaphoreDriver); + } + break; + + case 2: TimeLeft += 60; + break; + + case 3: if (TimeLeft > 60) + TimeLeft -= 60; + else + TimeLeft = 0; + break; + + case 4: if (BoilPower < 100) + BoilPower++; + break; + + case 5: if (BoilPower > 0) + BoilPower--; + break; + + default: break; + } + + if (TimeLeft == 0) { + Main_Screen = MAIN_AUTO_WHIRLPOOL9; + ESP_LOGI(TAG, "Boil is ready"); + } + break; + + case MAIN_AUTO_COOLING_H: + case MAIN_AUTO_COOLING_M: + case MAIN_AUTO_COOLING_C: + if (_Prompt) { + /* + * Prompt mode + */ + switch (Buttons_Scan()) { + case 0: _Prompt = false; + Buttons_Clear(); + break; + case 1: Main_Screen = MAIN_AUTO_DONE; + Buttons_Clear(); + return true; //goto startover; + break; + default: + break; + } + + if (! _Prompt) { + /* + * Starting cooling, setup the screen. + */ + Buttons_Clear(); + TFT_fillScreen(_bg); + if (Main_Screen == MAIN_AUTO_COOLING_H) { + stageTemp = 77.0; + } else if (Main_Screen == MAIN_AUTO_COOLING_M) { + stageTemp = 66.0; + } else { + stageTemp = recipe.CoolTemp; + } + CoolBeep = false; + ESP_LOGI(TAG, "Start cooling from %6.2f to %4.1f", ds18b20_state->mlt_temperature, stageTemp); + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_mode = MLT_MODE_OFF; + driver_state->mlt_sp = stageTemp; + xSemaphoreGive(xSemaphoreDriver); + } + log_annotation(ANNOTATION_STAGE, "Koelen"); + TopMessage("Koelen"); + MLT_info(71, 26, false); + Buttons_Add( 5, 200, 60, 40, "Stop", 0); + Buttons[0].dark = true; + Buttons_Add( 5, 26, 60, 40, "+1", 1); + Buttons_Add(255, 26, 60, 40, "-1", 2); + /* + * The next key is not a mistake, but we need a key entry which + * will later become the pump key. The keyscan routine will find + * the original key if pressed. + */ + Buttons_Add(255, 26, 60, 40, "-1", 3); + Buttons_Show(); + } + } else { + /* + * Not in prompt mode. + */ +#ifdef CONFIG_TEMP_SENSORS_SIMULATOR + if (Fake_MLT > 12.0) { + if (driver_state->pump_run) + Fake_MLT -= 0.00025 * (Fake_MLT - 12.0); + else + Fake_MLT -= 0.00015 * (Fake_MLT - 12.0); + } + if (xSemaphoreTake(xSemaphoreDS18B20, 10) == pdTRUE) { + ds18b20_state->mlt_temperature = ((int)(Fake_MLT * 16)) / 16.0; + xSemaphoreGive(xSemaphoreDS18B20); + } +#endif + MLT_info(71, 26, true); + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + /* + * If the pump safe temperature is reached, add the control button. + * Redefine key number 3 if it is at the position of key 2. + */ + if ((driver_state->mlt_pv < equipment.PumpMaxTemp) && (Buttons[3].x == Buttons[2].x) &&(Buttons[3].y == Buttons[2].y)) { + Buttons_Add(255, 200, 60, 40, "Pomp", 3); + Buttons_Show(); + } + xSemaphoreGive(xSemaphoreDriver); + } + switch (Buttons_Scan()) { + case 1: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (Main_Screen == MAIN_AUTO_COOLING_H) { + if (driver_state->mlt_sp < 77.0) + driver_state->mlt_sp += 1.0; + } else if (Main_Screen == MAIN_AUTO_COOLING_M) { + if (driver_state->mlt_sp < 66.0) + driver_state->mlt_sp += 1.0; + } else if (Main_Screen == MAIN_AUTO_COOLING_C) { + if (driver_state->mlt_sp < 30.0) + driver_state->mlt_sp += 1.0; + } + xSemaphoreGive(xSemaphoreDriver); + } + break; + + case 0: Buttons_Add( 60, 150, 90, 40, "Stoppen", 4); + Buttons[4].dark = true; + Buttons_Add(170, 150, 90, 40, "Sorry", 5); + Buttons_Show(); + break; + + case 2: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (Main_Screen == MAIN_AUTO_COOLING_H) { + if (driver_state->mlt_sp > 71.0) + driver_state->mlt_sp -= 1.0; + } else if (Main_Screen == MAIN_AUTO_COOLING_M) { + if (driver_state->mlt_sp > 60.0) + driver_state->mlt_sp -= 1.0; + } else if (Main_Screen == MAIN_AUTO_COOLING_C) { + if (driver_state->mlt_sp > 10.0) + driver_state->mlt_sp -= 1.0; + } + xSemaphoreGive(xSemaphoreDriver); + } + break; + + case 3: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (driver_state->mlt_pv < equipment.PumpMaxTemp) { + if (driver_state->pump_run) + driver_state->pump_run = 0; + else + driver_state->pump_run = 1; + } else { + driver_state->pump_run = 0; + } + runtime.PumpCooling = driver_state->pump_run; + updateRuntime = true; + xSemaphoreGive(xSemaphoreDriver); + } + break; + + case 4: Main_Screen++; + break; + + case 5: Buttons[4].x = Buttons[5].x = -1; + TFT_fillRect(60, 150, 200, 40, TFT_BLACK); + break; + + default: + break; + } + + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (! CoolBeep && (driver_state->mlt_pv < (driver_state->mlt_sp + 2.0))) { + SoundPlay(SOUND_Warn); + CoolBeep = true; + } + if (driver_state->mlt_pv <= driver_state->mlt_sp) { + SoundPlay(SOUND_TempReached); + Main_Screen++; + } + xSemaphoreGive(xSemaphoreDriver); + } + } + break; + + case MAIN_AUTO_WHIRLPOOL9: + case MAIN_AUTO_WHIRLPOOL7: + case MAIN_AUTO_WHIRLPOOL6: + case MAIN_AUTO_WHIRLPOOL2: + if (_Prompt) { + + switch (Buttons_Scan()) { + case 0: _Prompt = false; + break; + case 1: if (Main_Screen == MAIN_AUTO_WHIRLPOOL2) { + Main_Screen = MAIN_AUTO_DONE; + } else { + Main_Screen++; + } + Buttons_Clear(); + return true; //goto startover; + break; + default: + break; + } + + if (! _Prompt) { + /* + * Prepare the screen for the actual whirpool. + */ + Buttons_Clear(); + TFT_fillScreen(_bg); + if (Main_Screen == MAIN_AUTO_WHIRLPOOL9) { + TimeWhirlPool = recipe.Whirlpool9; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = 93.0; + driver_state->mlt_mode = MLT_MODE_PID; + xSemaphoreGive(xSemaphoreDriver); + } + TopMessage("Whirlpool 88..100"); + } else if (Main_Screen == MAIN_AUTO_WHIRLPOOL7) { + TimeWhirlPool = recipe.Whirlpool7; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = 74.0; + driver_state->mlt_mode = MLT_MODE_PID; + xSemaphoreGive(xSemaphoreDriver); + } + TopMessage("Whirlpool 71..77"); + } else if (Main_Screen == MAIN_AUTO_WHIRLPOOL6) { + TimeWhirlPool = recipe.Whirlpool6; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->mlt_sp = 63.0; + driver_state->mlt_mode = MLT_MODE_PID; + xSemaphoreGive(xSemaphoreDriver); + } + TopMessage("Whirlpool 60..66"); + } else { + TimeWhirlPool = recipe.Whirlpool2; + TopMessage("Koude whirlpool"); + } + if (Resume) { + TimeWhirlPool = runtime.StageTimeLeft; + } + + /* + * If the pump is allowed at the current temperature, turn it on. + */ + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->pump_run = (driver_state->mlt_pv < equipment.PumpMaxTemp) ? 1 : 0; + xSemaphoreGive(xSemaphoreDriver); + } + log_annotation(ANNOTATION_STAGE, "Whirlpool"); + + TimerSet(TimeWhirlPool * 60); + runtime.StageTimeLeft = TimeWhirlPool; + updateRuntime = true; + MLT_info(71, 26, false); + ESP_LOGI(TAG, "Whirlpool %d minutes, sp %4.1f", TimeWhirlPool, driver_state->mlt_sp); + Buttons_Add(255, 120, 60, 40, "+1m", 0); + Buttons_Add( 5, 120, 60, 40, "-1m", 1); + Buttons_Add(130, 200, 60, 40, "Pomp", 2); + Buttons_Show(); + } + } else { + /* + * Not running in prompt mode, do the whirlpool. + */ + if (TimeLeft == 120) { + /* + * Drop the temperature when whirlpool is almost ready. + * If we are lucky the heater element will cool down so + * the next cooling stage will not wast too much energy. + */ + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (Main_Screen == MAIN_AUTO_WHIRLPOOL9) + driver_state->mlt_sp = 88.0; + if (Main_Screen == MAIN_AUTO_WHIRLPOOL7) + driver_state->mlt_sp = 71.0; + if (Main_Screen == MAIN_AUTO_WHIRLPOOL6) + driver_state->mlt_sp = 60.0; + xSemaphoreGive(xSemaphoreDriver); + } + } + + MLT_info(71, 26, true); + TimerShow(TimeLeft, 65, 122); + switch (Buttons_Scan()) { + case 0: TimeLeft += 60; + break; + case 1: if (TimeLeft > 60) + TimeLeft -= 60; + else + TimeLeft = 0; + break; + case 2: if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + if (driver_state->mlt_pv < equipment.PumpMaxTemp) { + if (driver_state->pump_run) + driver_state->pump_run = 0; + else + driver_state->pump_run = 1; + } else { + driver_state->pump_run = 0; + } + xSemaphoreGive(xSemaphoreDriver); + } + } + + if (NewMinute) { + runtime.StageTimeLeft = TimeLeft / 60; + updateRuntime = true; + } + + if ((TimeLeft == 0)) { + if (Main_Screen == MAIN_AUTO_WHIRLPOOL9) { + Main_Screen = MAIN_AUTO_COOLING_H; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->pump_run = runtime.PumpCooling; + xSemaphoreGive(xSemaphoreDriver); + } + } else if (Main_Screen == MAIN_AUTO_WHIRLPOOL7) { + Main_Screen = MAIN_AUTO_COOLING_M; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->pump_run = runtime.PumpCooling; + xSemaphoreGive(xSemaphoreDriver); + } + } else if (Main_Screen == MAIN_AUTO_WHIRLPOOL6) { + Main_Screen = MAIN_AUTO_COOLING_C; + if (xSemaphoreTake(xSemaphoreDriver, 10) == pdTRUE) { + driver_state->pump_run = runtime.PumpCooling; + xSemaphoreGive(xSemaphoreDriver); + } + } else if (Main_Screen == MAIN_AUTO_WHIRLPOOL2) { + Main_Screen = MAIN_AUTO_DONE; + } + } + } + break; + + case MAIN_AUTO_DONE: + case MAIN_AUTO_ABORT: + if (Buttons_Scan() == 0) + Main_Screen = MAIN_MODE_FREE; + break; + + default: + break; + } + + return false; +} + +