How should I browse tags using the java API? Bugs?

Hi all.
I'm trying to write a module component which reads all tag configurations in ignition on module start (TODO: have it periodically rescan) in order to look for custom configuration attributes and act on them.

I'm encountering something odd; the java API isn't returning all of the tags I expect.

Using a python gateway event, I'm able to run:

def browse(prefix, node, next):
  if node is None:
    return
  prefix = prefix + "/" + (node.get("name") or "aryastark")  # A node has no name
  if "tags" not in node:
  	print "Tagchange script:", prefix, "=", {k:v for k,v in node.items() if k != "tags"}
  	return
  for child in node["tags"]:
  	next(prefix, child, next)

root = "[default]/Software/LatencyTest"  # Or whatever. Can also be emptystring.
print "Begin Tagchange script!", root
for node in system.tag.getConfiguration(root, True):
  print "Tagchange script: Toplevel", (node.get("name") or "aryastark")
  browse(root, node, browse)
print "End Tagchange script!"

(the combinator is because recursion didn't work without it).

However, when I run equivalent code (kotlin; it runs from my module's startup hook: )

            fun fetchAll(
                tagManager:GatewayTagManager,
                sink: (TagPath) -> Unit
            ) {
                fun browseNode(prefix: TagPath, tagConfig: TagConfigurationModel) {
                    val fullPath = prefix.getChildPath(tagConfig.path.toStringPartial())  // For some reason the tagConfig.path is a partial path, not a FQPath -- bug? Misnamed field? Needs doc either way.
                    when (tagConfig.type) {
                        TagObjectType.Folder, TagObjectType.Provider -> for (child in tagConfig.children) browseNode(fullPath, child)
                        TagObjectType.AtomicTag -> sink(BaseTag(service, tagConfig.path))
                        else -> logger.error("Unrecognized type $fullPath!")
                    }
                }
                val root = TagPathParser.parse("")  // I know that this is different, but it is a prefix of the python code...
                runBlocking {
                    for (provider in tagManager.tagProviders) {
                        for (firstGeneration in provider.getTagConfigsAsync(listOf(root), true, false).get(30, TimeUnit.SECONDS)) {
                            browseNode(root, firstGeneration)
                        }
                    }
                }
            }

Python has all of the tags I can see in the ignition designer.
However, java (kotlin...) is missing several random tags, with no apparent pattern between them!
From a spot check, this java code has e.g. [default]Software/LatencyTest/read/887/Data, but not [default]Software/LatencyTest/read/0/Data or [default]Software/LatencyTest/read/1/Data (which are afaik identical tags). It doesn't matter if they are initialized; they're all readonly, memory, etc.

At this point I guess I assume startup order: perhaps ignition doesn't load the full set of tags before loading modules or something, so that my late-running python code can see more than my (relatively) early-running java code.
Is this documented? Is there a method to call (or point during execution?) where I can get the same snapshot the designer & python code has of tags in my java module?

Thank you!

An update.
I changed the code to do this reload & print step every 15s after module load.
The error is persistent: the same keys are still (and always) missing; this means startup ordering is not implicated.

Help!! :slight_smile:

I pinged a developer more familiar with this, but he was out sick yesterday and I'm not sure if he'll be here today or not.

I saw you opened a support ticket for this as well, but I'll just let you know now that the support department doesn't assist with module development issues and there is no official support avenue, just the forum.

1 Like

The scripting functions and module tag apis end up calling the same functions, so I'm not sure where the discrepancy is. Can you send me a tag export so I can double check a few things? Also, it doesn't sound like you're using them, but TagObjectType.UdtInstance and TagObjectType.UdtType can also contain children tags, so if you run into them you'll be losing their children with current code.

My tags are: tags.json.zip (411.6 KB).
Note all desired tags do appear there.
More testing: The missing .../0/Data and .../1/Data tags might appear in my output logs... ... with the wrong names. I have two .../1/Data and zero .../0/Data in my output, this time -- still a bug, but a different bug!

Some questions that maybe indicate I'm doing something wrong:

  1. how do I actually generate the full path to the node recursively? I think that the TagConfigurationModel.getPath() is literally the same string as TagConfigurationModel.getName(), just wrapped in a (misleading?) TagPath; I think that what I should do is take in the parent TagPath and use parentTagPath.getChildPath(child.getName()) -- except in the case where parentTagPath.getPathLength() == 0 || parentTagPath.getLastPathComponent.isEmpty(), which means the parent was "" which means these are top level folders, and I should just use child.getPath() straight, because getChildPath doesn't handle that. True?
  2. What's the best way to get a specific instance of Property<*> (such as Property<DataType> named dataType so that I can read the tag's data type...)? Property is an interface, and the concrete subclass constructors are too specific to implementation for me to feel confident in constructing an instance to use to get information out of the TagConfigurationModel correctly. Currently I get an instance from the first object, cache it, and use it on the following objects -- acceptable or sketchy?

Totally agreed wrt support of structured tags in this code; gotta leave some problems for myself in the future.

  1. getPath() from TagConfigurationModel is relative to it's parent, so that's why you're only seeing the child name in there. I'd do what you mentioned and just use parentPath.getChildPath(child.getName()) to build the full path. You shouldn't have to worry about a child path with length == 0 if you use this method. Your starting parent path in this case will always be root
  2. WellKnownTagProps should contain the majority of Properties you're dealing with, if you have a custom property you'll have to create a BasicProperty with the specific value type you're expecting.
1 Like

Ok. I can say that this path (using child.getName) does work; if anyone else would find the code helpful, my version automatically recurses into (only) folders:

fun GatewayTagManager.streamTags(
    visit: (TagProvider) -> ((TagPath, TagConfigurationModel) -> Boolean)?
) {
    fun browseNode(visit: (TagPath, TagConfigurationModel)->Boolean, fullPath: TagPath, tagConfig: TagConfigurationModel) {
        if (visit(fullPath, tagConfig)) when (tagConfig.type) {
                TagObjectType.Folder -> {
                    for (child in tagConfig.children) {
                        browseNode(visit, fullPath.getChildPath(child.name), child)
                    }
                }
                else -> return
            }
        }
    }
    runBlocking {
        for (provider in tagProviders) {
            val root = TagPathParser.parse("[${provider.name}]")  // Though one might use some subdir in some cases...
            val visitor = visit(provider)
                ?: continue
            val firstGenerations = provider.getTagConfigsAsync(listOf(root), true, false).get(30, TimeUnit.SECONDS)
            for (firstGeneration in firstGenerations) {
                when (firstGeneration.type) {
                    TagObjectType.Provider -> for (secondGeneration in firstGeneration.children) {
                        browseNode(visitor, root.getChildPath(secondGeneration.name), secondGeneration)
                    }
                    else -> browseNode(visitor, root.getChildPath(firstGeneration.name), firstGeneration)
                }
            }
        }
    }
}