PREFACE
In what is my most ambition Ignition project yet, I am trying to build a recipe system that dynamically loads variables into SFC-based procedures (the SFCs are not generated dynamically). I started here and it rapidly grew out of the scope of any examples that I could find. I am running into lots of roadbumps of various jaggedness, but so far there are only a few I can't figure out. I've seen relatively little information about a project like this on the forums, so I'm laying out my project a bit more verbosely than might be strictly necessary for my immediate problems. Most of this writeup is for my own cognitive benefit, but if it helps anyone else that's great too!
To start: I try to develop in project dev followed by export to project base. I then discard overrides and save again (QOL REQUEST: when I export something to the project that it inherits from, please automatically "Discard Overrides" since they should at that point be identical). From that point, I am under the impression that dev should behave the same as base (but it doesn't always actually do this?)
Current test setup: I have a recursive SFC (and all the affilliated DB / Transaction group stuff, described below) that I am using to test my ability to have SFCs call other SFCs. I successfully had it functioning as expected until I exported the SFC from my dev project into its parent base project. Now, when I run it from the dev project without modifications (other than the required override to even access it in the Designer), its child SFCs are all not showing up in the Chart Control panel of the dev project. Upon investigation, it seems that they DO show up in the base Designer's Chart Control!
This direct misbehavior was the impetus for this whole post (article?). But then I started writing
PROBLEMS (and gotchas, to be expanded and resolved over time)
- I am confused about what projects are supposed to own SFCs. I understand that they run in the gateway entirely, and you have to jump through hoops to have them interact with a client via system commands (system.sfc.*Variables()). So I am perplexed that they seem to be owned by different projects.
- Enclosings don't seem to be able to be used dynamically (can't set path by reference), so I have start and monitor the SFC via scripts in Action blocks, which means that the script engine is now involved in the administration of the SFCs themselves (unless these are the same engines?). I was hoping I could get further with the built-in SFC capabilties.
- Debugging SFC errors is very annoying, the Chart Control shows like the first 12 characters of the error and the Gateway Logs basically only tell me which block was misbehaving with a stack that is very obtuse.
- Putting tables in another schema other than the default seems to break the ability of the Database Query Browser to generate queries quickly via double clicking them (doesn't include the schema), it makes it more annoying to find the recipes in the Table Browser because they arent alphebetized and can't be resorted, and also caused another problem that I have already forgotten because I gave up on this attempted relatively quickly.
- Enclosings default to "Cancel" mode, but so far I have almost always wanted them to be in "Blocking" mode. Not toggling this correctly has resulted in a lot of hair pulling for me.
PROJECT
I'll use my basic recursiveSoftwareTest_v1 SFC as an example. It loads some variables, waits 3 seconds to pretend to do work, calls another instance of itself, waits for that instance to return, waits another 3 seconds to pretend to do more work, then exits. The net result of calling this with a max_depth of 3 is that I have a root level SFC that runs in very close to exactly 18 seconds.
Because of problem 2), I have a couple of blocks that I need to copy paste around into all of the procedures that call other SFCs. First one loads (see load) tag data from some structured folders so that the block is agnostic to whatever SFC it is in. Next one runs a child object (see run_chidl) via system.sfc.startChart, and grabs / handles its instanceId so that it can be monitored, and the final one is an enclosing that loops until the chart identified with that instanceId is no longer in a state like "Running" "Starting" or "Initial" (see await_child).
DETAILS
# load
def onStart(chart, step)
# Load the parameters for this procedure
# they are pydatasets need to derefrence w/ [0]
params = [x[0] for x in system.db.runNamedQuery(
"base", "get_procedure_params",
{"procedure_name": chart.procedure_name})
]
# Load the tag values for this procedure
# RECIPE_ID NEEDS TO BE SELECTED BEFOREHAND TO LOAD CORRECTLY
# I know that this will only let me use a single loaded recipe per procedure
# Thats a problem for later
param_tags = [
"[HMI]SFC/procedures/{}/{}".format(chart.procedure_name, p)
for p in params
]
param_tag_values = system.tag.readBlocking(param_tags)
# Connect those values to this chart
for i, p in enumerate(params):
setattr(chart, p, param_tag_values[i].value)
# run_child
def onStart(chart, step)
if chart.current_depth+1 < chart.max_depth:
chart.child_chart_id = system.sfc.startChart(
"base", "procedures/{}".format(chart.child_chart), {
"procedure_name": chart.child_chart,
"current_depth": chart.current_depth+1
}
)
else:
chart.child_chart_id = None
await_child
# init
def onStart(chart, step)
chart.child_chart = chart.child
chart.child_chart_id = chart.parent['child_chart_id']
chart.logger = chart.parent.get('logger', None)
chart.child_chart_done = False
chart.n_awaits = 0
#await_child
def onStart(chart, step)
if type(chart.child_chart_id) is type(None):
chart.child_chart_done = True
return
charts = system.sfc.getRunningCharts("procedures/{}".format(chart.child_chart))
coi = [{"state": charts.getValueAt(row, "chartState"),
"id": charts.getValueAt(row, "instanceId")}
for row in range(charts.rowCount)
if chart.child_chart_id == charts.getValueAt(row, "instanceId")
]
if any([x == coi[0]['state'].toString() for x in ['Initial', 'Starting', 'Running']]):
chart.child_chart_done = False
else:
chart.child_chart_done = True
For storing my procedure and recipe specifications, I have a procedure table and
recipes_{procedure_name} tables. The procedure table contains the procedure_name as well as a JSON object representing the column names and types of the parameters in the procedure. A set of scripts generates the procedure entries and recipe tables from externally managed CSV files. The active parameters for a given SFC procedure is held in a tag folder that is linked to the database via a Transaction Group triggering on the recipe_id of a given procedure. This part works well enough so far, although programming the transaction groups has been a bit annoying as it requires generating them in XML and importing them via the legacy-ish system.groups.loadFromFile. I will eventually need a view system for controlling versions, probably.
# get_procedure_params
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'recipes_' || LOWER(:procedure_name)
AND column_name not like '%_ndx'
AND column_name not in ('t_stamp', 'procedure_name')
Example procedure entry + recipe table:
procedures
recipes_softwareTestRecursive_v1
tags
Congratulations if you got this far! Hopefully this was delivered in a comprehensible manner, I promise it was not written linearly.
My current main question is about problem 1), with project interitance. But I also am here for comments and (constructive) critiques. Especially with regards to the SFC system in general. I feel like it is not as polished as it could be, and circumventing its shortcomings consumes a large fraction of my attention for this project.