Managing Recipes with varying number of steps

Hey all,

I've been playing with doing some recipe management via Ignition lately and while I can get simple recipe types to work fine with a standard transaction group I'm having a hard time wrapping my head around doing more complex forms of recipes. Mainly, recipes that can have varying numbers of steps. For example, some of our equipement has multiple steps (or rows) consisting of multiple functions (columns). Something like this:


So, the number of steps on a recipe can change and the information associated with that step can also change. This makes every recipe basically a table in the database. Depending on the equipment we can have many many recipes.

I’m not sure what the best way to handle this in Ignition would be? It doesn’t seem like something a normal transaction group would be able to handle (but I’m not very fluent with transaction groups so I may be wrong). Is there a transaction group that handles entire tables? I’m just wondering if anyone else has run into something similar and would be willing to give me some insight into what to do with this situation. Thanks!

A transaction group can certainly handle this case. Every recipe will have a different table and a different “Standard” transaction group. Hopefully the PLC will trigger you when it needs the next step so we can use that as the trigger of the transaction group. So the group will only run when the PLC needs another step. You can use the step value to get the appropriate row out of the database table by using the “update/select custom row” using the where clause:stepnum = {Path/To/StepTag}

Ok… I see what you’re saying, that’s a ton of tables and transaction groups though. I guess I could condense it by having a recipe number column and a step number column and have the select clause say where recipenumber = XXX and stepnumber = XXX. Then I could put everything in one table.

Is there also a way to make transaction groups indirect? For example, we have 20 some smokehouses, it would be most ideal if I could have one transaction group set up for a smokehouse and then call that transaction group with indirect tag paths to whichever smokehouse is calling for a recipe change.

If I set it up the way you’re saying I’ll have 20 smokehouse transaction groups X 50+ Recipes = 1000+ Transaction groups… wow…

Perhaps there is a better way to do it through scripting?

There is no way to make transaction groups indirect. In this case you only need one transaction group per smokehouse if you add the recipenumber and stepnumber to the database table. Scripting is another route you can go down. You can do everything in a single tag change script.

Is it still the case that transaction groups cannot be indirect? I am looking to do the same as above, loading a single recipe into a variable tag path.

I'm not sure that I understand the original problem but I'd be inclined to structure one recipe table and give it two leading columns:

  • Recipe name or number.
  • Recipe step.

Now a query can return all the steps for a recipe:

SELECT recipeName, recipeStep, param1, param2, param3
FROM recipes
WHERE recipeName = :valRecipeName
ORDER BY recipeStep

(:valRecipeName is a query parameter.)

I would need to be able to apply a recipe to a variable tag path.

Location/{var}/param1
Location/{var}/param2
Location/{var}/param3

Ideally, under Table Action, I could update/select a key/value pair based on a tag that varies the tag paths in my Group Items.

No, transaction groups cannot do this. You will need to script your data transfer.

1 Like

I ended up having two tables, one table was the main recipe information such as ID, recipe name, creation date, etc... The other table had all of the recipe steps in it with each step having a recipe ID number equal to the ID on the main table.

I was using Opto 22 so I would query this data, create a CSV file, FTP and write to the SD card on the controller. I have done similar with Siemens PLCs as well but then you have a set number of steps and just loop through and write to each tag with the {step number} indirection.

def writeRecipiesToSH(SHIPDict, recipeID="all"):
	query = "SELECT ID, RecipeName from SH_Recipe_Table"
	if recipeID != "all":
		query += " WHERE ID=%s" % (recipeID)
	recipes = system.db.runQuery(query)
	recipeDict = {}
	for recipe in recipes:
		recipeDict.update({recipe[0]:recipe[1]})
	recipeData = _getRecipeFileData(recipeDict)
	
	import ftplib
	import time
	SHErrorList = []
	DLLogger = system.util.getLogger("SH Download Logger")
	
	if shared.genFunctions.changeDefaultDir("SHRecipes"):
		for SH in SHIPDict:
			try:
				startTime = time.time()
				# Log into the Opto units and write the file to the SD Card
				ftp = ftplib.FTP(SHIPDict[SH])
				ftp.login()
				ftp.cwd("/sdcard0")
				folderName = "recipe"
				# Check to see if the recipe directory exists on the SH SD card.
				if folderName in ftp.nlst():
					ftp.cwd("/sdcard0/recipe")
				# If the recipe directory doesn't exist then we are going to try and create it.
				else:
					DLLogger.infof("Recipe directory does not exist on Smokehouse %s, directory is being created...", SH)
					ftp.mkd("recipe")
					ftp.cwd("/sdcard0/recipe")
				logTime = time.time() - startTime
				DLLogger.infof("Time to FTP into Smokehouse #%s: %s Seconds", SH, str(logTime))
			except:
				SHErrorList.append(SH)
				DLLogger.errorf("Error logging into, changing directory or creating directory on Smokehouse: %s", SH)
			else:
				for recipe in recipeData:
					startTime = time.time()
					try:
						#Open up a new file
						fileName = recipe+".TXT"
						file = open(fileName, "wb+")
					except:
						DLLogger.errorf("Error creating text file for recipe: %s", recipe)
					else:
						#write recipe data to file
						file.write(recipeData[recipe])
						logTime = time.time() - startTime
						DLLogger.infof("Time to create text file for recipe %s: %s Seconds", recipe, str(logTime))
						startTime = time.time()
											
						# Move file pointer to the begging of the file and begin file transfer
						file.seek(0,0)
						try:
							# store the open file to the Opto SD card via FTP
							ftp.storbinary("STOR "+fileName, file)
						except:
							SHErrorList.append(SH)
							file.close()
							DLLogger.errorf("Error storing recipe on Smokehouse: %s", SH)
						else:
							file.close()
							logTime = time.time() - startTime
							DLLogger.infof("Time to write recipe %s to Smokehouse #%s: %s Seconds", recipe, SH, str(logTime))
				ftp.quit()			
				
		return SHErrorList
		
	else:
		DLLogger.errorf("Could not change default directory")

def _getRecipeFileData(recipeDict):
	"""This will query the database for all recipe step information and return a dictionary of recipe name and CSV file data
	   Takes in a dictionary in the form {RecipeID:Recipe Name}
	   Returns a dictionary in the form {Recipe Name:CSV File Data (with headers)}
	"""
	query = """SELECT StepNumber, SteamCook, SmokeCook, Temperature, HumidityCook, Humidity, InternalCook, InternalTemp, BlowerHigh, BlowerLow, ExhMotor, Time, ExhDamper, FreshAirDamper,
           PositiveClosedDamper, AirForSmoke, LiquidSmoke, Alarm, Shower, Comments
           FROM SH_Recipe_Steps
           WHERE RecipeID = ?
        """
	recipeFiles = {}
	for recipe in recipeDict:
		recipeName = recipeDict[recipe]
		stepData = system.db.runPrepQuery(query, [recipe])
		stepData = system.dataset.toDataSet(stepData)
		stepsString = system.dataset.toCSV(stepData, showHeaders=True)
		recipeFiles.update({recipeName:stepsString})
	return recipeFiles