main/recipes.c

Thu, 29 Jul 2021 22:36:17 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Thu, 29 Jul 2021 22:36:17 +0200
changeset 114
1413c4c5cd8c
parent 93
24cb415af787
child 116
bafc2d6a0790
permissions
-rw-r--r--

Fixed Brewfather beerxml import.

/**
 * @file recipes.c
 * @brief Recipes management. If new beerxml recipes are detected in
 *        the /recipe directory on the SD card they will be imported 
 *        when you enter the recipes menu. After successfull import the
 *        xml file extensions will be changed to xok to prevent that
 *        the recipe is imported again. After the import the recipe
 *	  editor is shown.
 */

#include "config.h"


extern sButton			Buttons[MAXBUTTONS];		///< Buttons definitions
extern int			Main_Screen;			///< Current screen

bool				r_UpdateRec = false;		///< Update record flag
int				r_CurrentRec = 1;		///< Current record
int				r_Records = 1;			///< Total records
int				r_Imported = 0;			///< Total imported
char                            _xml_element[10][32];		///< XML element array
int                             _xml_depth;			///< Current depths
char                            _xml_add_name[64];		///< Mash name
int                             _xml_add_type;			///< Mash type 
int                             _xml_add_time;			///< Mash rest time
int				_xml_add_ramp;			///< Mash ramp time
float				_xml_add_temp;			///< Mash start temperature
float				_xml_add_end;			///< Mash end temperature
float				_xml_add_amount;		///< Mash infusion amount
float				_xml_add_infusion;		///< Mash infusion temperature
float				_xml_tun_temp;			///< TUN temperature
bool                            _xml_add_valid;			///< Add is valid

static const char               *TAG = "recipes";

char				char_data_buffer[1024];		///< Data buffer
size_t				offs;				///< Offset in buffer
bool				overflow;			///< Overflow in buffer

extern const char		*mashTypes[];



/**
 * @brief Add addition to the recipe.
 * @param Name The addition name.
 * @param Type The addition type.
 * @param Time The addition time to add.
 */
void Addition_Add(char *Name, uint8_t Type, uint16_t Time)
{
    if (! recipe.Additions) {
	// No entries yet, add the first one.
    	snprintf(recipe.Addition[recipe.Additions].Name, 63, "%s", Name);
    	recipe.Addition[recipe.Additions].Type = Type;
    	recipe.Addition[recipe.Additions].Time = Time;
    	recipe.Additions++;
	return;
    }

    // See if we already got one with the same time.
    for (int i = 0; i < recipe.Additions; i++) {
	if (recipe.Addition[i].Time == Time) {
	    // Yes, update the name.
	    strncat(recipe.Addition[i].Name, ", ", 63 - strlen(recipe.Addition[i].Name));
	    strncat(recipe.Addition[i].Name, Name, 63 - strlen(recipe.Addition[i].Name));
	    return;
	}
    }

    // A new entry and we already have some. Add it and keep the list sorted.
    for (int i = 0; i < recipe.Additions; i++) {
	if (Time > recipe.Addition[i].Time) {
	    // Make room
	    for (int j = i; j < recipe.Additions; j++) {
		sprintf(recipe.Addition[j+1].Name, "%s", recipe.Addition[j].Name);
		recipe.Addition[j+1].Type = recipe.Addition[j].Type;
		recipe.Addition[j+1].Time = recipe.Addition[j].Time;
	    }
	    snprintf(recipe.Addition[i].Name, 63, "%s", Name);
	    recipe.Addition[i].Type = Type;
	    recipe.Addition[i].Time = Time;
	    recipe.Additions++;
	    return;
	}
    }

    // Just append.
    snprintf(recipe.Addition[recipe.Additions].Name, 63, "%s", Name);
    recipe.Addition[recipe.Additions].Type = Type;
    recipe.Addition[recipe.Additions].Time = Time;
    recipe.Additions++;
}



/**
 * @brief Reset the parser buffer.
 */
void reset_char_data_buffer (void)
{
    offs = 0;
    overflow = false;
}



/**
 * @brief Datahandler get bytes into the data buffer.
 */
void char_data(void *userData, const XML_Char *s, int len)
{
    if (!overflow) {
        if (len + offs >= sizeof(char_data_buffer) ) {
            overflow = true;
        } else {
            memcpy(char_data_buffer + offs, s, len);
            offs += len;
        }
    }
}


/**
 * @brief Process the XML parser data buffer.
 */
void process_char_data_buffer (void)
{
    bool        allspace = true;

    if (offs > 0) {
        char_data_buffer[ offs ] = '\0';

        /*
         * If all spaces and control characters, the data is invalid.
         */
        for (int i = 0; i < strlen(char_data_buffer); i++) {
            if (char_data_buffer[i] > ' ')
                allspace = false;
        }
        if (allspace)
            return;

        if ((_xml_depth > 2) && (strcmp(_xml_element[0], "RECIPES") == 0) && (strcmp(_xml_element[1], "RECIPE") == 0)) {
            /*
             * We are in a recipe
             */
            if ((_xml_depth == 3) && (strcmp("NAME", _xml_element[2]) == 0)) {
		recipe.Name[0] = '\0';
                strncat(recipe.Name, char_data_buffer, 127);
            } else if ((_xml_depth == 3) && (strcmp("BOIL_TIME", _xml_element[2]) == 0)) {
                recipe.BoilTime = atoi(char_data_buffer);
	    } else if ((_xml_depth == 3) && (strcmp("BMS_COOLING_TO", _xml_element[2]) == 0)) {
		recipe.CoolTemp = atoi(char_data_buffer);
            } else if ((_xml_depth == 5) && (strcmp("HOPS", _xml_element[2]) == 0) && (strcmp("HOP", _xml_element[3]) == 0)) {
		/*
		 * Hops that are added during the boil.
		 * But check for whirlpool hops too.
		 */
                if (strcmp("NAME", _xml_element[4]) == 0) {
		    _xml_add_name[0] = '\0';
                    strncat(_xml_add_name, char_data_buffer, 63);
                } else if (strcmp("USE", _xml_element[4]) == 0) {
                    _xml_add_valid = (strcmp("Boil", char_data_buffer) == 0);   // Only "Boil" is a valid hop
                    if (strcmp("Aroma", char_data_buffer) == 0) {
                        recipe.Whirlpool7 = 30;
                    }
                } else if (strcmp("TIME", _xml_element[4]) == 0) {
                    _xml_add_time = atoi(char_data_buffer);
                }
            } else if ((_xml_depth >= 3) && (strcmp("STYLE", _xml_element[2]) == 0)) {
                // Ignore
            } else if ((_xml_depth >= 3) && (strcmp("EQUIPMENT", _xml_element[2]) == 0)) {
                // Ignora
            } else if ((_xml_depth >= 3) && (strcmp("YEASTS", _xml_element[2]) == 0)) {
                // Ignore
            } else if ((_xml_depth >= 3) && (strcmp("WATERS", _xml_element[2]) == 0)) {
                // Ignore
            } else if ((_xml_depth == 5) && (strcmp("FERMENTABLES", _xml_element[2]) == 0) && (strcmp("FERMENTABLE", _xml_element[3]) == 0)) {
		/*
		 * Fermentabes that must be added during the boil.
		 */
                if (strcmp("NAME", _xml_element[4]) == 0) {
		    _xml_add_name[0] = '\0';
                    strncat(_xml_add_name, char_data_buffer, 63);
                } else if (strcmp("TYPE", _xml_element[4]) == 0) {
                    if ((strcmp("Sugar", char_data_buffer) == 0)/* || (strcmp("Adjunct", char_data_buffer) == 0)*/) {
                        _xml_add_type = ADDITION_FERMENTABLE;
                        _xml_add_time = 10;
                    } else {
                        _xml_add_type = -1;
                    }
                } else if (strcmp("ADD_AFTER_BOIL", _xml_element[4]) == 0) {
                    _xml_add_valid = (strcmp("FALSE", char_data_buffer) == 0);
                }
            } else if ((_xml_depth == 5) && (strcmp("MISCS", _xml_element[2]) == 0) && (strcmp("MISC", _xml_element[3]) == 0)) {
		/*
		 * Check for Misc additions to add during the boil.
		 */
		if (strcmp("NAME", _xml_element[4]) == 0) {
		    _xml_add_name[0] = '\0';
		    strncat(_xml_add_name, char_data_buffer, 63);
		} else if (strcmp("USE", _xml_element[4]) == 0) {
		    _xml_add_valid = (strcmp("Boil", char_data_buffer) == 0);   // Only "Boil" is a valid hop.
		} else if (strcmp("TYPE", _xml_element[4]) == 0) {
		    if (strcmp("Spice", char_data_buffer) == 0)
			_xml_add_type = ADDITION_SPICE;
		    if (strcmp("Fining", char_data_buffer) == 0)
			_xml_add_type = ADDITION_FINING;
		    if (strcmp("Herb", char_data_buffer) == 0)
			_xml_add_type = ADDITION_HERB;
		    if (strcmp("Flavor", char_data_buffer) == 0)
			_xml_add_type = ADDITION_FLAVOR;
		    if (strcmp("Other", char_data_buffer) == 0)
			_xml_add_type = ADDITION_OTHER;
		} else if (strcmp("TIME", _xml_element[4]) == 0) {
		    _xml_add_time = atoi(char_data_buffer);
		}
            } else if ((_xml_depth >= 4) && (strcmp("MASH", _xml_element[2]) == 0)) {
		if  ((_xml_depth >= 6) && (strcmp("MASH_STEP", _xml_element[4]) == 0)) {
		    if (strcmp("NAME", _xml_element[5]) == 0) {
			_xml_add_name[0] = '\0';
			strncat(_xml_add_name, char_data_buffer, 63);
		    } else if (strcmp("TYPE", _xml_element[5]) == 0) {
			// Step_temp Infusion Decoction
			_xml_add_valid = (strcmp("Step_temp", char_data_buffer) == 0);
			if (strcmp("Infusion", char_data_buffer) == 0)
			    _xml_add_type = MASHTYPE_INFUSION;
			else if (strcmp("Temperature", char_data_buffer) == 0)
			    _xml_add_type = MASHTYPE_TEMPERATURE;
			else if (strcmp("Decoction", char_data_buffer) == 0)
			    _xml_add_type = MASHTYPE_DECOCTION;
		    } else if (strcmp("STEP_TEMP", _xml_element[5]) == 0) {
			_xml_add_temp = atof(char_data_buffer);
		    } else if (strcmp("STEP_TIME", _xml_element[5]) == 0) {
			_xml_add_time = atoi(char_data_buffer);
		    } else if (strcmp("RAMP_TIME", _xml_element[5]) == 0) {
			_xml_add_ramp = atoi(char_data_buffer);
		    } else if (strcmp("INFUSE_AMOUNT", _xml_element[5]) == 0) {
			_xml_add_amount = atof(char_data_buffer);
		    } else if (strcmp("INFUSE_TEMP", _xml_element[5]) == 0) {
			_xml_add_infusion = atof(char_data_buffer);
		    } else if (strcmp("DECOCTION_AMT", _xml_element[5]) == 0) {
			_xml_add_amount = atof(char_data_buffer);
		    } else if (strcmp("END_TEMP", _xml_element[5]) == 0) {
			_xml_add_end = atof(char_data_buffer);
		    }
		} else if ((_xml_depth >= 4) && (strcmp("TUN_TEMP", _xml_element[3]) == 0)) {
		    // Save this and check later if this is the strike temperature.
		    _xml_tun_temp = atof(char_data_buffer);
		} else if ((_xml_depth >= 4) && (strcmp("SPARGE_TEMP", _xml_element[3]) == 0)) {
		    recipe.SpargeTemp = atof(char_data_buffer);
		}
            }
        }
    }
}



/**
 * @brief Start new XML element.
 * @param userData
 * @param name XML element name
 * @param attr XML element attribute
 */
void startElement(void *userData, const char *name, const char **attr)
{
    sprintf(_xml_element[_xml_depth], "%s", name);
    _xml_depth++;
    process_char_data_buffer();
    reset_char_data_buffer();
}



/**
 * @brief End XML element
 * @param userData
 * @param name XML element name
 */
void endElement(void *userData, const char *name)
{
    process_char_data_buffer();
    reset_char_data_buffer();
    if ((_xml_depth == 4) && (strcmp("HOP", _xml_element[3]) == 0)) {
        if (_xml_add_valid) {
	    Addition_Add(_xml_add_name, ADDITION_HOP, _xml_add_time);
        }
    }
    if ((_xml_depth == 4) && (strcmp("FERMENTABLE", _xml_element[3]) == 0)) {
        if (_xml_add_valid && (_xml_add_type == ADDITION_FERMENTABLE)) {
	    Addition_Add(_xml_add_name, _xml_add_type, _xml_add_time);
        }
    }
    if ((_xml_depth == 4) && (strcmp("MISC", _xml_element[3]) == 0)) {
	if (_xml_add_valid) {
	    Addition_Add(_xml_add_name, _xml_add_type, _xml_add_time);
	}
    }
    if ((_xml_depth == 5) && (strcmp("MASH_STEP", _xml_element[4]) == 0)) {
	recipe.MashStep[recipe.Mashsteps].Name[0] = '\0';
	strncat(recipe.MashStep[recipe.Mashsteps].Name, _xml_add_name, 31);
	recipe.MashStep[recipe.Mashsteps].Step_temp = _xml_add_temp;
	recipe.MashStep[recipe.Mashsteps].Step_time = _xml_add_time;
	recipe.MashStep[recipe.Mashsteps].Ramp_time = _xml_add_ramp;
	recipe.MashStep[recipe.Mashsteps].End_temp = (_xml_add_end > 0.0) ?  _xml_add_end : _xml_add_temp;
	/*
	 * Fix problems created by Brewfather and possible others that say that
	 * the first mash step is a temperature step. It just isn't.
	 */
	if (_xml_add_type == MASHTYPE_TEMPERATURE && recipe.Mashsteps == 0) {
	    _xml_add_type = MASHTYPE_INFUSION;
	    _xml_add_infusion = _xml_add_temp + 1.25;
	    recipe.MashStep[recipe.Mashsteps].Ramp_time = 1;
	}
	recipe.MashStep[recipe.Mashsteps].Type = _xml_add_type;
	if (_xml_add_ramp == 0 && recipe.Mashsteps) {
	    recipe.MashStep[recipe.Mashsteps].Ramp_time = (uint16_t)(_xml_add_temp - recipe.MashStep[recipe.Mashsteps - 1].End_temp) + 1;
	}
	if (_xml_add_type == MASHTYPE_INFUSION) {
	    recipe.MashStep[recipe.Mashsteps].Infuse_temp = _xml_add_infusion;
	    recipe.MashStep[recipe.Mashsteps].Infuse_amount = _xml_add_amount;
	} else if (_xml_add_type == MASHTYPE_DECOCTION) {
	    recipe.MashStep[recipe.Mashsteps].Infuse_temp = 98.0;
	    recipe.MashStep[recipe.Mashsteps].Infuse_amount = _xml_add_amount;
	} else {
	    recipe.MashStep[recipe.Mashsteps].Infuse_temp = 0.0;
	    recipe.MashStep[recipe.Mashsteps].Infuse_amount = 0.0;
	}
	recipe.Mashsteps++;
    }
    _xml_depth--;
}



int ParseRecipe(char *fn, char *code)
{
    char buf[512];

    memset(&recipe, 0, recipe_hdr.recsize);
    for (int i = 0; i < recipe_hdr.mashmax; i++)
	recipe.MashStep[i].Type = 255;
    XML_Parser parser = XML_ParserCreate(NULL);

    int done;
    _xml_depth = 0;
    _xml_tun_temp = 0.0;
    _xml_add_infusion = 0.0;
    recipe.CoolTemp = 20.0;
    recipe.SpargeTemp = equipment.TempHLT;

    XML_SetElementHandler(parser, startElement, endElement);
    XML_SetCharacterDataHandler(parser, char_data);

    FILE *fp = fopen(fn, "r");

    do {
        int len = (int)fread(buf, 1, sizeof(buf), fp);
        done = len < sizeof(buf);
        if (XML_Parse(parser, buf, len, done) == XML_STATUS_ERROR) {
            ESP_LOGE(TAG, "%s at line %5lu", XML_ErrorString(XML_GetErrorCode(parser)), XML_GetCurrentLineNumber(parser));
            return 1;
        }
        vTaskDelay(2 / portTICK_PERIOD_MS);
    } while (!done);

    XML_ParserFree(parser);
    fclose(fp);

    /*
     * Fix missing things.
     */
    snprintf(recipe.Code, 31, "%s", code);
    // The code is created from the recipe filename.
    for (int i = 0; i < strlen(recipe.Code); i++) {
	if ((recipe.Code[i] == ' ') || (recipe.Code[i] == '.')) {
	    recipe.Code[i] = '\0';
	    break;
	}
    }

#if 0
    printf("Recipe: %s\n", recipe.Name);
    printf("Code  : %s\n", recipe.Code);
    printf("Boil time %d minutes\n", recipe.BoilTime);
    printf("n Stepname                       T  temp   end time ramp  inft  infa\n");
    printf("- ------------------------------ - ----- ----- ---- ---- ----- -----\n");
    for (int i = 0; i < 8; i++) {
        if (recipe.MashStep[i].Step_time) {
            printf("%d %-30s %d %5.2f %5.2f %4d %4d %5.2f %5.2f\n", i, recipe.MashStep[i].Name, recipe.MashStep[i].Type,
			    recipe.MashStep[i].Step_temp, recipe.MashStep[i].End_temp,
                            recipe.MashStep[i].Step_time, recipe.MashStep[i].Ramp_time,
			    recipe.MashStep[i].Infuse_temp, recipe.MashStep[i].Infuse_amount);
        }
    }
    printf("%d additions\n", recipe.Additions);
    printf("n Addition name                                                   t Tim\n");
    printf("- --------------------------------------------------------------- - ---\n");
    for (int i = 0; i < recipe.Additions; i++) {
        printf("%d %-63s %d %3d\n", i, recipe.Addition[i].Name, recipe.Addition[i].Type, recipe.Addition[i].Time);
    }
    printf("Cooltemp %5.2f\n", recipe.CoolTemp);
    if (recipe.Whirlpool9)
    	printf("Whirlpool9 %d\n", recipe.Whirlpool9);
    if (recipe.Whirlpool7)
    	printf("Whirlpool7 %d\n", recipe.Whirlpool7);
    if (recipe.Whirlpool6)
    	printf("Whirlpool6 %d\n", recipe.Whirlpool6);
    if (recipe.Whirlpool2)
    	printf("Whirlpool2 %d\n", recipe.Whirlpool2);
    printf("SpargeTemp %5.2f\n", recipe.SpargeTemp);
#endif

    return 0;
}



/*
 * Recipes init function, only runs once a new screen is entered.
 */
void Recipes_Init(void)
{
    FILE		*f;
    DIR			*dir;
    struct dirent	*de;
    size_t		bytes;
    uint8_t		*dst;
    uint16_t		y;
    char		filename[288], newname[288];
    int			rc;

    switch (Main_Screen) {
	case MAIN_TOOLS_RECIPE:
			TopMessage((char *)"Recepten importeren");
			r_Imported = 0;
			TFT_setFont(DEFAULT_FONT, NULL);
			y = 28;
			if ((dir = opendir("/sdcard/recipe"))) {
			    de = readdir(dir);
			    while (de) {
				if (strstr(de->d_name, ".xml") || strstr(de->d_name, ".XML")) {
				    _fg = TFT_YELLOW;
				    TFT_print(de->d_name, 2, y);
				    snprintf(filename, 287, "/sdcard/recipe/%s", de->d_name);
				    snprintf(newname, 287, "/sdcard/recipe/%s", de->d_name);
				    newname[strlen(newname) -2] = 'o';
				    newname[strlen(newname) -1] = 'k';
				    rc = ParseRecipe(filename, de->d_name);
				    ESP_LOGI(TAG, "Recipe %s parsed, rc=%d", filename, rc);
				    if (rc == 0) {
					append_recipe();
					rename(filename, newname);
				    	_fg = TFT_GREEN;
				    	TFT_print((char *)" Ok", LASTX, y);
				    } else {
				    	_fg = TFT_RED;
				    	TFT_print((char *)" Fout", LASTX, y);
				    }
				    r_Imported++;	// Count them all
				    y += 16;
				}
				de = readdir(dir);
			    }
			    closedir(dir);
			}

			f = fopen("/spiffs/etc/recipe.conf", "r");
			fseek(f, recipe_hdr.hdrsize, SEEK_SET);
			dst = (uint8_t*)&recipe;
			r_Records = 0;
			while ((bytes = fread(dst, 1, recipe_hdr.recsize, f))) {
			    r_Records++;
			}
			fclose(f);
			// Load the default record.
			r_CurrentRec = config.RecipeRec;
			r_UpdateRec = true;
			break;

	case MAIN_TOOLS_RECIPE_EDIT:
			break;

	default:	break;
    }
}



/*
 * Recipes management loop, non-blocking.
 */
void Recipes_Loop(void)
{
    uint32_t	crc1, crc2;
    uint8_t	*dst;
    uint16_t	y;
    char	tmp[64];
    float	mintemp, maxtemp;
    int		i;

    switch (Main_Screen) {
	case MAIN_TOOLS_RECIPE:
			if (r_Imported) {
			    Buttons_Clear();
			    Buttons_Add(135, 210, 50, 30, (char *)"Ok"  , 0);
			    Buttons_Show();

			    while (true) {
				if (Buttons_Scan() == 0)
				    break;
				vTaskDelay(20 / portTICK_PERIOD_MS);
			    }
			    Buttons_Clear();
			    r_Imported = 0;
			}
			if (r_UpdateRec) {
			    _bg = TFT_BLACK;
			    TFT_fillScreen(_bg);
			    TFT_resetclipwin();
			    TopMessage((char *)"Recepten");
			    r_UpdateRec = false;
			    read_recipe(r_CurrentRec);
			    TFT_setFont(DEFAULT_FONT, NULL);
			    ShowText(2, 28, (char *)"Naam", recipe.Name);
			    ShowText(2, 44, (char *)"Code", recipe.Code);
			    ShowInteger(162, 44, (char *)"Record", NULL, r_CurrentRec);
			    ShowInteger(2, 60, (char *)"Kooktijd", (char *)" min", recipe.BoilTime);
			    ShowFloat(162, 60, (char *)"Koel tot", (char *)" C", recipe.CoolTemp, 2);
			    ShowFloat(2, 76, (char *)"Maisch in", (char *)" C", recipe.MashStep[0].Infuse_temp, 3);
			    ShowFloat(162, 76, (char *)"Spoelwater", (char *)" C", recipe.SpargeTemp, 2);
			    y =  92;
			    _fg = TFT_WHITE;
			    TFT_print((char *)"Maisch stap", 2, y);
			    TFT_print((char *)"T", 180, y);
			    TFT_print((char *)"Temp.", 200, y);
			    TFT_print((char *)"Rust", 280, y);
			    _fg = TFT_YELLOW;
			    y += 16;
			    for (i = 0; i < recipe_hdr.mashmax; i++) {
				ESP_LOGD(TAG, "%2d %-31s %3d %5.2f %5.2f %2d %2d %7.4f %6.3f", i, recipe.MashStep[i].Name, recipe.MashStep[i].Type,
					recipe.MashStep[i].Step_temp, recipe.MashStep[i].End_temp, recipe.MashStep[i].Step_time, recipe.MashStep[i].Ramp_time,
					recipe.MashStep[i].Infuse_temp, recipe.MashStep[i].Infuse_amount);
				if (recipe.MashStep[i].Type != 255) {
				    TFT_print(recipe.MashStep[i].Name, 2, y);
				    strcpy(tmp, mashTypes[recipe.MashStep[i].Type]);
				    tmp[1] = '\0';
				    TFT_print(tmp, 180, y);
				    sprintf(tmp, "%.1f %.1f", recipe.MashStep[i].Step_temp, recipe.MashStep[i].End_temp);
				    TFT_print(tmp, 200, y);
				    sprintf(tmp, "%2d m", recipe.MashStep[i].Step_time);
				    TFT_print(tmp, 280, y);
				    y += 16;
				}
			    }
			    if (recipe.BoilTime) {
			    	if (recipe.Additions) {
				    _fg = TFT_YELLOW;
				    sprintf(tmp, "%d ", recipe.Additions);
				    TFT_print(tmp, 2, y);
				    _fg = TFT_WHITE;
				    TFT_print((char *)"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((char *)" minuten", LASTX, y);
			    	} else {
				    _fg = TFT_WHITE;
				    TFT_print((char *)"Geen hop toevoegingen.", 2, y);
			    	}
			    	y += 16;
			    }
			    if (recipe.Whirlpool9 || recipe.Whirlpool7 || recipe.Whirlpool6 || recipe.Whirlpool2) {
				_fg = TFT_WHITE;
				TFT_print((char *)"Whirlpool ", 2, y);
				_fg = TFT_YELLOW;
				if (recipe.Whirlpool9)
				    TFT_print((char *)"88+ ", LASTX, y);
				if (recipe.Whirlpool7)
				    TFT_print((char *)"71+ ", LASTX, y);
				if (recipe.Whirlpool6)
				    TFT_print((char *)"60+ ", LASTX, y);
				if (recipe.Whirlpool2)
				    TFT_print((char *)"koud", LASTX, y);
			    }

			    Buttons_Clear();
			    Buttons_Add(  0, 210, 45, 30, (char *)"Ok"  , 0);
			    Buttons_Add( 46, 210, 45, 30, (char *)"+"   , 1);
			    if (r_CurrentRec != config.RecipeRec)
				Buttons_Add( 92, 210, 45, 30, (char *)"-", 2);
			    else
				Buttons_Add( 92, 210, 45, 30, (char *)""    , 2);
			    if (r_CurrentRec > 1)
				Buttons_Add(138, 210, 45, 30, (char *)"<", 3);
			    else
				Buttons_Add(138, 210, 45, 30, (char *)"", 3);
			    if (r_CurrentRec < r_Records)
				Buttons_Add(184, 210, 45, 30, (char *)">", 4);
			    else
				Buttons_Add(184, 210, 45, 30, (char *)"", 4);
			    if (r_CurrentRec != config.RecipeRec)
				Buttons_Add(230, 210, 45, 30, (char *)"Std", 5);
			    else
				Buttons_Add(230, 210, 45, 30, (char *)"", 5);
			    Buttons_Add(276, 210, 45, 30, (char *)"Ed"  , 6);
			    Buttons[0].dark = true;
			    Buttons_Show();
			} /* if (r_UpdateRec) */
			switch (Buttons_Scan()) {
			    case 0:	Main_Screen = MAIN_TOOLS;
					break;

			    case 1:	memset(&recipe, 0, sizeof(recipe)); // new recipe
					for (i = 1; i < recipe_hdr.mashmax; i++)
					    recipe.MashStep[i].Type = 255;
					sprintf(recipe.Name, "Recept %d", r_Records + 1);
					sprintf(recipe.Code, "00%d", r_Records + 1);
					sprintf(recipe.MashStep[0].Name, "Maischen");
					recipe.MashStep[0].Type = MASHTYPE_INFUSION;
					recipe.MashStep[0].Step_temp = recipe.MashStep[0].End_temp = 67.0;
					recipe.MashStep[0].Infuse_temp = 67.5;
					recipe.MashStep[0].Infuse_amount = 15.0;
					recipe.MashStep[0].Step_time = 75;
					recipe.MashStep[0].Ramp_time = 1;
					sprintf(recipe.MashStep[1].Name, "Mash-out");
					recipe.MashStep[1].Type = MASHTYPE_TEMPERATURE;
					recipe.MashStep[1].Step_temp = recipe.MashStep[1].End_temp = 78.0;
					recipe.MashStep[1].Step_time = 10;
					recipe.MashStep[1].Ramp_time = 13;
					recipe.Mashsteps = 2;
					recipe.BoilTime = 60;
					recipe.Additions = 2;
					sprintf(recipe.Addition[0].Name, "Bitterhop");
					recipe.Addition[0].Time = 60;
					recipe.Addition[0].Type = ADDITION_HOP;
					sprintf(recipe.Addition[1].Name, "Aromahop");
					recipe.Addition[1].Time = 10;
					recipe.Addition[1].Type = ADDITION_HOP;
					recipe.CoolTemp = 20.0;
					recipe.Whirlpool9 = 0;
					recipe.Whirlpool7 = 0;
					recipe.Whirlpool6 = 0;
					recipe.Whirlpool2 = 0;
					recipe.SpargeTemp = 85.0;
					append_recipe();
					r_Records++;
					r_CurrentRec = r_Records;
					r_UpdateRec = true;
					ESP_LOGI(TAG, "New recipe record %d", r_CurrentRec);
					break;

			    case 2:	if ((r_CurrentRec != config.RecipeRec) && (r_Records > 1)) {
					    _bg = TFT_BLACK;
                            		     TFT_fillScreen(_bg);
                            		    TFT_resetclipwin();
					    TopMessage((char *)"Recept verwijderen");
					    delete_recipe(r_CurrentRec);
					    r_Records--;
					    if (r_CurrentRec >= r_Records)
						r_CurrentRec = r_Records;
					    r_UpdateRec = true;
					}
					break;

			    case 3:	if (r_CurrentRec > 1) {
					    r_CurrentRec--;
					    r_UpdateRec = true;
					}
					break;

			    case 4:	if (r_CurrentRec < r_Records) {
				    	    r_CurrentRec++;
					    r_UpdateRec = true;
			    		}
			    		break;

			    case 5:	if (r_CurrentRec != config.RecipeRec) {
					    config.RecipeRec = r_CurrentRec;
					    write_config();
					    r_UpdateRec = true;
					    ESP_LOGI(TAG, "Recipe %d `%s' set as default", r_CurrentRec, recipe.Name);
					}
					break;

			    case 6:	Main_Screen = MAIN_TOOLS_RECIPE_EDIT;
					break;

			    default:	break;
			}
			break;

	case MAIN_TOOLS_RECIPE_EDIT:
			dst = (uint8_t*)&recipe;
			crc1 = crc32_le(0, dst, sizeof(recipe));

			EditText((char *)"Naam", recipe.Name, 31);
			EditText((char *)"Code", recipe.Code, 31);
			EditUint8((char *)"Maisch stappen", &recipe.Mashsteps, 1, 6);
			for (i = 0; i < recipe.Mashsteps; i++) {
			    int step = i + 1;
			    if (i == (recipe.Mashsteps - 1)) {
				mintemp = 75.0;
				maxtemp = 80.0;
			    } else {
				mintemp = 35.0;
				maxtemp = 74.0;
			    }
			    if (recipe.MashStep[i].Type == 255) {
				// A new step, set default values.
				recipe.MashStep[i].Type = MASHTYPE_TEMPERATURE;
				recipe.MashStep[i].Step_time = 20;
				if (i == 0) {
				    recipe.MashStep[i].Type = MASHTYPE_INFUSION;
				    recipe.MashStep[i].Step_temp = recipe.MashStep[i].End_temp = 62.0;
				    recipe.MashStep[i].Infuse_temp = 62.5;
				    recipe.MashStep[i].Infuse_amount = 15;
				    recipe.MashStep[i].Ramp_time = 1;
				} else if (i == (recipe.Mashsteps - 1)) {
				    recipe.MashStep[i].Step_temp = recipe.MashStep[i].End_temp = 78.0;
				    recipe.MashStep[i].Step_time = 10;
				    recipe.MashStep[i].Ramp_time = (int)(recipe.MashStep[i].Step_temp - recipe.MashStep[i - 1].Step_temp + 2);
				} else {
				    recipe.MashStep[i].Step_temp = recipe.MashStep[i].End_temp = recipe.MashStep[i - 1].Step_temp + 2.0;
				    recipe.MashStep[i].Ramp_time = (int)(recipe.MashStep[i].Step_temp - recipe.MashStep[i - 1].Step_temp + 2);
				}
			    }
			    sprintf(tmp, "Maisch stap %d naam", step);
			    EditText(tmp, recipe.MashStep[i].Name, 31);
			    EditMashType(&recipe.MashStep[i].Type);
			    sprintf(tmp, "Maisch stap %d start temperatuur", step);
			    EditFloat(tmp, &recipe.MashStep[i].Step_temp, mintemp, maxtemp, 2);
			    // Round to 0.25 values
			    recipe.MashStep[i].Step_temp = ((int)(recipe.MashStep[i].Step_temp * 4)) / 4.0;
			    sprintf(tmp, "Maisch stap %d eind temperatuur", step);
			    EditFloat(tmp, &recipe.MashStep[i].End_temp, mintemp, maxtemp, 2);
			    recipe.MashStep[i].End_temp = ((int)(recipe.MashStep[i].End_temp * 4)) / 4.0;
			    sprintf(tmp, "Maisch stap %d rusttijd in minuten", step);
			    EditUint16(tmp, &recipe.MashStep[i].Step_time, 1, 480);
			    if (recipe.MashStep[i].Type == MASHTYPE_INFUSION) {
				sprintf(tmp, "Stap %d infusie temperatuur", step);
				EditFloat(tmp, &recipe.MashStep[i].Infuse_temp, 10, 99, 3);
				recipe.MashStep[i].Infuse_temp = ((int)(recipe.MashStep[i].Infuse_temp * 16)) / 16.0;
				sprintf(tmp, "Stap %d infusie volume", step);
				EditFloat(tmp, &recipe.MashStep[i].Infuse_amount, 0.5, 1000.0, 3);
				recipe.MashStep[i].Ramp_time = 2;
			    } else if (recipe.MashStep[i].Type == MASHTYPE_DECOCTION) {
				recipe.MashStep[i].Infuse_temp = 0.0;
				recipe.MashStep[i].Ramp_time = 2;
				sprintf(tmp, "Stap %d decoctie volume", step);
                                EditFloat(tmp, &recipe.MashStep[i].Infuse_amount, 0.5, 1000.0, 3);
			    } else {
				recipe.MashStep[i].Infuse_temp = recipe.MashStep[i].Infuse_amount = 0.0;
				sprintf(tmp, "Stap %d opwarm tijd", step);
				EditUint16(tmp, &recipe.MashStep[i].Ramp_time, 1, 480);
			    }
			}
			for (i = recipe.Mashsteps; i < recipe_hdr.mashmax; i++) {
			    recipe.MashStep[i].Type = 255;
			    recipe.MashStep[i].Step_temp = recipe.MashStep[i].End_temp = recipe.MashStep[i].Infuse_temp = recipe.MashStep[i].Infuse_amount = 0.0;
			    recipe.MashStep[i].Step_time = recipe.MashStep[i].Ramp_time = 0;
			    recipe.MashStep[i].Name[0] = '\0';
			}

			EditUint16((char *)"Kook tijd in minuten", &recipe.BoilTime, 0, 480);
			if (recipe.BoilTime) {
			    EditUint8((char *)"Hop/kruiden toevoegingen", &recipe.Additions, 1, 10);
			    for (uint8_t i = 0; i < recipe.Additions; i++) {
			    	sprintf(tmp, "Toevoeging %d naam", i+1);
			    	if (strlen(recipe.Addition[i].Name) == 00) {
				    sprintf(recipe.Addition[i].Name, "Hop %d", (int)i+1);
			    	}
			    	EditText(tmp, recipe.Addition[i].Name, 40);
			    	sprintf(tmp, "Toevoeging %d tijd", i+1);
			    	if (i == 0) {
			    	    EditUint16(tmp, &recipe.Addition[i].Time, 0, recipe.BoilTime);
			    	} else {
				    EditUint16(tmp, &recipe.Addition[i].Time, 0, recipe.Addition[i-1].Time - 1);
			    	}
			    	recipe.Addition[i].Type = ADDITION_HOP;
			    }
			} else {
			    recipe.Additions = 0;
			}

			EditFloat((char *)"Koel temperatuur", &recipe.CoolTemp, 10, 45, 2);
			recipe.CoolTemp = ((int)(recipe.CoolTemp * 4)) / 4.0;
			EditFloat((char *)"Spoelwater temperatuur", &recipe.SpargeTemp, 75, 98, 2);
			recipe.SpargeTemp = ((int)(recipe.SpargeTemp * 4)) / 4.0;
			EditUint16((char *)"Whirlpool 88..100 graden, 0 = niet", &recipe.Whirlpool9, 0, 120);
			EditUint16((char *)"Whirlpool 71..77 graden, 0 = niet", &recipe.Whirlpool7, 0, 120);
			EditUint16((char *)"Whirlpool 60..66 graden, 0 = niet", &recipe.Whirlpool6, 0, 120);
			EditUint16((char *)"Whirlpool koud, 0 = niet", &recipe.Whirlpool2, 0, 120);

			crc2 = crc32_le(0, dst, sizeof(recipe));
			if ((crc1 != crc2) && Confirm((char *)"Gewijzigd, opslaan?", (char *)"Ja", (char *)"Nee")) {
			    write_recipe(r_CurrentRec);
			}
			Main_Screen = MAIN_TOOLS_RECIPE;
			break;

	default:	break;
    }
}

mercurial