main/recipes.c

Fri, 28 Jun 2024 15:33:24 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Fri, 28 Jun 2024 15:33:24 +0200
branch
idf 5.1
changeset 137
e0f50087c909
parent 129
31f9d3e4a85f
permissions
-rw-r--r--

Fixed changing runtime datarecord size during switching between IDF 4.2 and 5.1. Fixed wiping the /spiffs filesystem. The directory listing from the SD card doesn't overwrite parts of the screen anymore. Solved the slow speed issue with the SD card. Try to force the SD card to operate at 20 MHz. More project settings changed to improve performance and memory usage.

/**
 * @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
extern my_config_t		config;
extern my_equipment_t		equipment;
extern my_recipe_hdr_t		recipe_hdr;
extern my_recipe_t		recipe;

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];		///< Addition name
int                             _xml_add_type;			///< Addition 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 (Type == ADDITION_HOP_AROMA && Time > 0) {
	/*
	 * Whirlpool hop.
	 */
	recipe.Whirlpool7 = Time;
	return;
    }
    if (Type == ADDITION_HOP_AROMA)
	Type = ADDITION_HOP_BOIL;	/* Flame-out hop, change to boil with zero 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 = recipe.Additions; j > i; j--) {
		sprintf(recipe.Addition[j].Name, "%s", recipe.Addition[j-1].Name);
		recipe.Addition[j].Type = recipe.Addition[j-1].Type;
		recipe.Addition[j].Time = recipe.Addition[j-1].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 = atof(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 "Aroma" hops too.
		 */
                if (strcmp("NAME", _xml_element[4]) == 0) {
		    _xml_add_name[0] = '\0';
                    strncat(_xml_add_name, char_data_buffer, 63);
		    _xml_add_time = 0;	// reset
		    _xml_add_valid = false;
                } else if (strcmp("USE", _xml_element[4]) == 0) {
		    if (strcmp("Boil", char_data_buffer) == 0) {
			_xml_add_valid = true;
			_xml_add_type = ADDITION_HOP_BOIL;
		    } else if (strcmp("Aroma", char_data_buffer) == 0) {
			_xml_add_type = ADDITION_HOP_AROMA;
			_xml_add_valid = true;
                    }
                } 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, _xml_add_type, _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;
	    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) {
	    if (recipe.Mashsteps == 0 && _xml_add_infusion < _xml_add_temp) {
		recipe.MashStep[recipe.Mashsteps].Infuse_temp = _xml_add_temp + 1.25;
	    } else {
	    	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  : %d minutes\n", recipe.BoilTime);
    printf("n Stepname                       T  temp   end time ramp  inft  infa\n");
    printf("- ------------------------------ - ----- ----- ---- ---- ----- -----\n");
    for (int i = 0; i < recipe.Mashsteps; i++) {
        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("\n%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_BOIL;
					sprintf(recipe.Addition[1].Name, "Aromahop");
					recipe.Addition[1].Time = 10;
					recipe.Addition[1].Type = ADDITION_HOP_BOIL;
					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)) && (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_BOIL;
			    }
			} 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