[Solved] Need help with system.tag.browse and diagnostic tool - Bit State Change Locator

Hello, I am open sourcing this with the hopes of speeding up development and giving it to anyone who might need a tool like this. I plan to place this on the public template cloud when it is complete. It worked ok, but am trying to improve it and am interested in what anyone has to say about the code. Feel free to critique and offer suggestions. The template’s concept and unresolved issues are listed below.

Bit State Change Locator

Concept:

A diagnostic tool designed to locate the timestamp of a boolean tag’s state change and use that timestamp to query all tags in a selected folder at that selected timestamp ± a time buffer in seconds.

The idea is to be able to diagnose the causes or effects of a selected tag’s state change.

Notable Components:

  • 2 Tag Tree Browsers
    • First tag tree is to select the tag you want to query to find a state change timestamp.
    • Second tag tree is to select a folder to browse through and query all boolean tags within that folder.
  • 2 Power Tables
    • First power table displays selected tag results with 2 columns, “state changed to” and “t_stamp”
    • Second power table displays the selected folder’s browse results with 5 columns of information: “Tag Path”, “Name”, “Timestamp”, “Value Changed to”, and “Documentation”.
  • 2 Spinners (time buffer in seconds)
    • A negative and positive time buffer selector in seconds.
  • A Numeric Text Field and Drop Down
    • “Browse history from the last x (minutes or hours)” - User input into the numerical text field the amount of “hours or minutes” selected with the drop down.
  • 3 Labels
    • Used to display the selected tag path, selected timestamp, and selected folder path.
  • 2 Buttons
    • First button is to query selected tag from configured state history from the last ## minutes or hours.
    • Second button is to browse and query tags from selected folder at selected timestamp ± the time buffer configuration.

Unresolved Issues:

  1. This was working, designed in 7.9.10, but only used it to recursively search through specific folders with only Boolean OPC tags. I would like this to be able to search through folders with UDTs and query the UDT’s boolean OPC tags.

  2. I am trying to use 8.0’s system.tag.browse instead of 7.9’s system.tag.browseSimple. Previously I was able to iterate through the system.tag.browseSimple’s list. The new system.tag.browse returns an object that I cannot iterate through. I am not sure how or where to convert the browse results into a list of tag paths to search.

Additional Thoughts:

  1. I understand the dangers of recursively searching through folders, but here it necessary. Is there a safer way to go about this recursive search and query?

  2. The tag browse tree can alternate from real time to historical tag paths. Originally I was using the historical tag path mode for the 2nd (folder selection) tag tree. But the historical tag path then needs to be parsed back to a regular tag path for the system.tag.queryTagHistory function. Is there any reason to do this one way or the other. All booleans are being logged on change and scanned at 1000ms. I see no reason to use the historical tag path mode as it adds complication to the code.

Template View:

Code Snips:

  1. First Button “Search For State Change” script. This works well, but I’ll take any input anyone has.
tag = [event.source.parent.getComponent('Label').text]

#Use dropdown to select rangeMinutes or rangeHours.
if event.source.parent.getComponent('Dropdown 1').selectedValue == 1:
	query = system.tag.queryTagHistory(paths=tag,
	calculations=['Count'],
	rangeMinutes=event.source.parent.getComponent('Numeric Text Field').intValue)
	
elif event.source.parent.getComponent('Dropdown 1').selectedValue == 2:
	query = system.tag.queryTagHistory(paths=tag,
	calculations=['Count'],
	rangeHours=event.source.parent.getComponent('Numeric Text Field').intValue)
	
#Creates a dataset with a row for each time the tag changed state in the time specified.
event.source.parent.parent.getComponent('Power Table').data = query
  1. Second Button “Search Selected Folder” script. Here is where the problems are coming from the defined browse() function. I pulled it from the example in the user manual on the system.tag.browse() page and tried modifying it to append a list unsuccessfully.
#grab the "Power Table" dataset
tableData = event.source.parent.parent.getComponent('Power Table').data
selectedRow = event.source.parent.parent.getComponent('Power Table').selectedRow


#grab time buffer values and the time stamp from 1st column of the selected row from the dataset.
negBuffer = event.source.parent.getComponent('Spinner').intValue
posBuffer = event.source.parent.getComponent('Spinner 1').intValue
date = system.date.parse(tableData.getValueAt(selectedRow, 0))


#start and end date time buffer to create a time window to search.
sd = system.date.addSeconds(date, negBuffer)
ed = system.date.addSeconds(date, posBuffer)


#selects the folder to search through.
folder = event.source.parent.getComponent('Label').text


#browse through the chosen folder and queries every tag at the specified timestamp (+/- {x} second), creates oneRow, a row with 5 columns, for every flagged tag and appends each row to the list "rows".
tagPaths = []
def browse(path, filter):
	results = system.tag.browse(path, filter)
	for result in results:
		tagPaths.append(result.getPath())
#        print result.getPath()
		if result['hasChildren'] == True:
			browse(result['fullPath'], filter)
browse(folder, {})


tags = list(tagPaths)
rows = []	

for tag in tags:
	check = system.tag.queryTagHistory(paths=["[default]"+ tag], calculations=['Count'], startDate=sd, endDate=ed)
	#if a change in state is detected, append a new row with the tag path, the tag name, timestamp, value changed to, and documentation.
	if check.rowCount >= 1:
		for x in range(check.rowCount):
			documentation = tag.fullPath + ".Documentation"
			doc = system.tag.read(documentation)
			oneRow = [tag.fullPath, tag.name, check.getValueAt(x,0), check.getValueAt(x,1), doc.value]
			rows.append(oneRow)


			
#Create the headers and datset. Populate 'Power Table 1' with the dataset.
headers = ['Path', 'Name', 'Time Stamp', 'Value Changed To', 'Documentation']
data = system.dataset.toDataSet(headers, rows)
table = event.source.parent.parent.getComponent('Power Table 1')
table.data = data
event.source.parent.parent.getComponent('Power Table 1').data = data

So, if I'm right, you intend to use it to log effects inside the PLC when a certain trigger happens (like a something passes before a photocell).

That can indeed be a very handy tool. Thanks for sharing.

Although it's bad practice to use this in an operator screen (locking up the interface for an operator will cause frustration), it's not such a big issue if you do it for diagnostics, and only technical people are supposed to use it.

Not intended for logging, just a tool for trouble shooting. It’s not for an operator screen. We had an issue where a remote control was misconfigured and unitentionally causing machinery to move. We didn’t know where the signal was coming from and so I built this to search for and catch the offending input.

So I got the code working for the second button. Although I think it could be optimized, here it is for anyone who might want it. I am iterating through a tag list and querying per tag because each tag might have multiple results and I’m not sure how to handle the result if I load a list of tags into the query.

#grab the "Power Table" dataset
tableData = event.source.parent.parent.getComponent('Power Table').data
selectedRow = event.source.parent.parent.getComponent('Power Table').selectedRow


#grab the time stamp from 1st column of the selected row from the dataset.
negBuffer = event.source.parent.getComponent('Spinner').intValue
posBuffer = event.source.parent.getComponent('Spinner 1').intValue
date = system.date.parse(tableData.getValueAt(selectedRow, 0))


#start and end date time buffer to create a time window to search.
sd = system.date.addSeconds(date, negBuffer)
ed = system.date.addSeconds(date, posBuffer)


#selects the folder to search through.
folder = event.source.parent.getComponent('Label').text


#browse through the chosen folder and queries every tag at the specified timestamp (+/- {x} second), creates oneRow, a row with 5 columns, for every flagged tag and appends each row to the list "rows".
tagPaths = []
def browse(path, filter):
	results = system.tag.browse(path, filter)
	for result in results.getResults():
		if result['hasChildren'] == False:
			tagPaths.append(result['fullPath'])
	
		if result['hasChildren'] == True:
			browse(result['fullPath'], filter)
browse(folder, {'dataType':'boolean'})


tags = tagPaths
rows = []	

for tag in tags:
	check = system.tag.queryTagHistory(paths=[tag], calculations=['Count'], startDate=sd, endDate=ed)
	#if a change in state is detected, append a new row with the tag path, the tag name, timestamp, value changed to, and documentation.
	if check.rowCount >= 1:
		for x in range(check.rowCount):
			doc = system.tag.readBlocking([(tag.toString() + ".Documentation")])
			name = system.tag.readBlocking([(tag.toString() + ".Name")])
			oneRow = [tag.toString(), name[0].value, check.getValueAt(x,0), check.getValueAt(x,1), doc[0].value]
			rows.append(oneRow)


			
#Create the headers and datset. Populate 'Power Table 1' with the dataset.
headers = ['Path', 'Name', 'Time Stamp', 'Value Changed To', 'Documentation']
data = system.dataset.toDataSet(headers, rows)
table = event.source.parent.parent.getComponent('Power Table 1')
table.data = data
event.source.parent.parent.getComponent('Power Table 1').data = data