Professional OPC
Development Tools

logos

Online Forums

Technical support is provided through Support Forums below. Anybody can view them; you need to Register/Login to our site (see links in upper right corner) in order to Post questions. You do not have to be a licensed user of our product.

Please read Rules for forum posts before reporting your issue or asking a question. OPC Labs team is actively monitoring the forums, and replies as soon as possible. Various technical information can also be found in our Knowledge Base. For your convenience, we have also assembled a Frequently Asked Questions page.

Do not use the Contact page for technical issues.

AMapping ( and Reading) objects within arrays using BrowsePaths

More
07 May 2020 07:03 #8472 by support
Hello,

I think the confusion may come from the fact that there are two areas to understand - and they behave differently:

a) On the OPC UA level, practically everything has a namespace with it. That is, node Ids include a namespace, and qualified names (used as browse names in browse path elements) also include namespace. So, if you have e.g. an absolute browse path, there are possibly many namespaces involved just in this one browse path: The namespace of the starting node, the namespaces that go with each element in the path, and also the namespace of the reference types between them. And the namespaces has truly be transmitted on the wire with the OPC request and responses (for efficiency, as indexes to namespace table).

So, in order to properly describe what happens within OPC UA, one needs to list all these namespaces.

This is a great design, because it allows all kinds of composition and aggregation scenarios: Shortly, the OPC UA models can be freely extended (new nodes, browse names etc. added), without a risk of conflict. All the "new" things can be given a unique namespace URI, and no collisions will ever appear.

In reality, however, it is quite possible that one namespace or just a few namespaces will be used all around. That's where the second second area kicks in:

b) The tools (such as QuickOPC) then try to make it easier for you by allowing you to *not* specify all these namespaces everywhere, explicitly. In the code examples we discussed, this has manifested itself in two ways:

- If you specify a default namespace when parsing the browse paths, you do not have to specify the namespace with each its element.
- If you specify a namespace by the [UANamespace] attribute at some level (such as on a type) in Live Mapping, it propagates itself automatically to deeper levels (until possibly overriden by a different [UANamespace] there).

Re 1) The above explanation now allows me to describe what happens in the Boiler example. This code:
    [UANamespace("http://opcfoundation.org/UA/Boiler/")]
    [UAType]
    class Boiler
    {
        // Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.
 
        [UANode(BrowsePath = "/PipeX001")]
        public BoilerInputPipe InputPipe = new BoilerInputPipe();
 
        [UANode(BrowsePath = "/DrumX001")]
        public BoilerDrum Drum = new BoilerDrum();
 
        [UANode(BrowsePath = "/PipeX002")]
        public BoilerOutputPipe OutputPipe = new BoilerOutputPipe();
 
        [UANode(BrowsePath = "/FCX001")]
        public FlowController FlowController = new FlowController();
 
        [UANode(BrowsePath = "/LCX001")]
        public LevelController LevelController = new LevelController();
 
        [UANode(BrowsePath = "/CCX001")]
        public CustomController CustomController = new CustomController();
    }

is just a shorter way of writing
    [UAType]
    class Boiler
    {
        // Specifying BrowsePath-s here only because we have named the class members differently from OPC node names.
 
        [UANamespace("http://opcfoundation.org/UA/Boiler/")]
        [UANode(BrowsePath = "/PipeX001")]
        public BoilerInputPipe InputPipe = new BoilerInputPipe();
 
        [UANamespace("http://opcfoundation.org/UA/Boiler/")]
        [UANode(BrowsePath = "/DrumX001")]
        public BoilerDrum Drum = new BoilerDrum();
 
        [UANamespace("http://opcfoundation.org/UA/Boiler/")]
        [UANode(BrowsePath = "/PipeX002")]
        public BoilerOutputPipe OutputPipe = new BoilerOutputPipe();
 
        [UANamespace("http://opcfoundation.org/UA/Boiler/")]
        [UANode(BrowsePath = "/FCX001")]
        public FlowController FlowController = new FlowController();
 
        [UANamespace("http://opcfoundation.org/UA/Boiler/")]
        [UANode(BrowsePath = "/LCX001")]
        public LevelController LevelController = new LevelController();
 
        [UANamespace("http://opcfoundation.org/UA/Boiler/")]
        [UANode(BrowsePath = "/CCX001")]
        public CustomController CustomController = new CustomController();
    }

That is, if you have a common namespace, you can save yourself from specifying it everywhere, by stating it just once, one level upwards (here, on the type).

Regarding the question about [UAType]: This attribute is mainly just for marking the types that are involved in the Live Mapping. It can be used for optimizations and checking internally by QuickOPC. There isn't more to it than this, do not worry too much thinking about it...

Re 2) It is possible that in this case the Live Mapping won't be of much benefit.

Best regards

Please Log in or Create an account to join the conversation.

More
05 May 2020 16:15 #8469 by cadomanis
Thanks for the very clear explanation on these topics. I think I have a good idea of the direction we should head in to make best use of the tool.

Two follow up questions.

1) You mention that the Namespace and BrowsePath items surrounding the UANode is very important in the LiveMapping scenario. I am wondering if you can explain the Class level [UAType] and how its UANamespace attribute factors in. I am confident that our node level items - in this case, the various properties within the class, would always be very consistent, but I think the class level namespace might be different with different server installations. Even in the Boiler example, it seems that having that Namespace attribute on the entire class is needed, even though you also pass it in with UAMappingContext? Clearly there is something here I am not understanding.

2) The majority of our interactions from the code to the Server will be predominantly done via method calls, with the need to only monitor a small number of items into each of our classes. In this case, is there any real benefit to the LiveMapping versus just having each class instance make a subscription to its variables for reading?

Thanks again. C-

Please Log in or Create an account to join the conversation.

More
05 May 2020 12:47 #8467 by support
Hello,
I will start somewhat from the end.

Yes, if you choose the Live Mapping approach, most things need to be "fixed" - i.e. known during development time, and that includes the namespaces that are part of node Ids and qualified names (in browse paths). They need to be explicitly written out in the attributes on the classes & members. That may actually be one of the major deciding factors: Live Mapping makes the code shorter and more elegant *if* you know how the objects are structured and their elements identified upfront (you can still use one C# class to map to multiple instance of UA object, such as Boiler1, Boiler2 etc.) If you don't know that upfront, Live Mapping is not for you (though you can use part of it, "a type-less mapping, example: opclabs.doc-that.com/files/onlinedocs/QuickOpc/Latest/User%2...ss%20mapping%20and%20read.html ).

Now to the namespaces, and default namespace:
A namespace is part of every node ID, and is part of every browse name (and browse path consists of browse names). That is, if you have a browse path of 5 elements (something like Element1/Element2/Element3/Element4/Element5), each of these elements is actually a namespace URI plus the "short" name like Element1. In general case, the namespace can be different for each element. In order to represent the browse path in full, one should actually list the namespace together with each element.. The problem with it is that it would look terrible and won't be easy to work with. Here comes the idea of default namespace: When you specify the default namespace, when parsing the browse path, you do not have to write it down with each element. You would then explicitly need to include the namespace URI inside the browse path only with elements whose namespace is different from the default. But in reality, on the OPC level, the namespace is always there, with each element.

Regarding the "starting point": Browse paths come actually in two forms:
- relative,
- absolute.

Relative browse path is just a sequence of browse elements, where each element specified the type of reference to follow, and then browse name that identifies the "node to go next". In QuickOPC, there is no dedicated type for it; any IEnumerable<> (such as array etc.) of UABrowseNodeElement.

Relative path does not refer to any concrete node without being also given a starting node.

Absolute browse path is a starting node plus relative browse path. In QuickOPC, it is represented by the UABrowsePath class.

The paths you have used ("[ObjectsFolder]/...") were absolute browse paths. In QuickOPC, there are methods to work (parse, ...) both with absolute and relative paths.

The typical usage scenario of Live Mapping is with OPC UA Object Types (image a Boiler). OPC UA Object Type defines a structure of Variable nodes, and the references between them (i.e. the reference types and browse names) stay the same for ALL instances of that object. That means that if you know the starting node of a particular object instance, you can use relative browse paths to reach its Variables, and the relative browse path will stay the same even if you switch to a different object instance. The main thing here is the [UANode] attribute and its BrowsePath parameter. Whatever you put in here becomes part of the relative browse path that is ultimately used by QuickOPC to reach the annotated variable. The "starting node" (the OPC UA object instance, e.g. Boiler1, Boiler 3, Boiler 5) is either given in development time using attributes, or it can be determined in runtime using mapping context (as in our examples). The browse paths to the individual mapped variables are then constructed by Live Mapping automatically from the [UANode] BrowsePath parameters.

I hope this helps.
The following user(s) said Thank You: cadomanis

Please Log in or Create an account to join the conversation.

More
04 May 2020 17:45 #8462 by cadomanis
Again, thanks for the concise and clear answer. I think I understand better now the points you have made.

One point I think I am still struggling with is the idea of the "starting point" and the need for specifying a DefaultNamespace.

In an absolute reference like
 [ObjectsFolder]/PLC1/OpcCommunication/OpcEffect/SetPosition
is not the [ObjectsFolder] defined as a root object? An so when I in addition pass in a more Namepsace URI like
urn:BeckhoffAutomation:Ua:PLC1
which really is the reference to the PLC1 portion of the larger browse path, am I not giving the same path twice in some ways?

The real reason I am asking is that in my testing and working through examples, I do not know how I would ever resolve the need to decorate my classes that are a UAType with a UANamespace attribute prior to using them in a mapping. Even in the included Boiler example, the Boiler class has a similar decoration in the class.
    [UAType, UANamespace("urn:BeckhoffAutomation:Ua:PLC1")]
    public class OpcEffectLinear : INotifyPropertyChanged
    {

But this would require understanding that Namespace prior to runtime? It seems to really allow the code to be flexible, all references - in Classes, mapping, method calls, etc. - should be done using browse paths?

Thanks again. C-

Please Log in or Create an account to join the conversation.

More
04 May 2020 17:28 - 05 May 2020 04:56 #8461 by support
Hello,
here you go - only the modified part:
var methodCallArguments = new[]
            {
                new UACallArguments(endpointDescriptor,
                    //"ns=4;s=OpcCommunication.OpcEffect",
                    browsePathParser.Parse("[ObjectsFolder]/PLC1/OpcCommunication/OpcEffect"),
                    //"ns=4;s=OpcCommunication.OpcEffect#SetPosition",
                    browsePathParser.Parse("[ObjectsFolder]/PLC1/OpcCommunication/OpcEffect/SetPosition"),
 
                    inputs,
                    typeCodes)
            };

Here is what you can do to determine the browse path quickly and reliably - I know you are using UaExpert. It is quite intuitive: The browse path is *how you browse* for the node you want to reach.
- Start the browse path with [ObjectsFolder].
- Following the nodes in the "Address space" pane of the OpcExpert that lead from the Objects to the node you want to reach, put name of each node as a separate element into the browse path (that is the what you see in "BrowseName" of each node, *not* anything from its NodeId). In many cases the BrowseName is the same as DisplayName so you can simply take the node names from Address Space tree.
- Separate the elements with '/' ('.' is stricter, and there are other delimiters, too, but '/' normally works)

Note that what I just wrote is a big simplification - it makes assumptions such as that
- The starting point you want to browse from actually is the Object folder. But there may be many browsing paths for the same target Node ID - depending on where you (want to) start from
- The namespace is the same for all qualified named in the browse path
- You only want to follow forward references
- It is OK to follow all hierarchical references (this is what '/' signifies)

You may want to read:
- opclabs.doc-that.com/files/onlinedocs/QuickOpc/Latest/User%2...wse%20Paths%20in%20OPC-UA.html
- opclabs.doc-that.com/files/onlinedocs/QuickOpc/Latest/User%2...%20Browse%20Path%20Format.html
- opclabs.doc-that.com/files/onlinedocs/QuickOpc/Latest/User%2...0Browse%20Path%20Elements.html


You could use our Connectivity Explorer and it will show you the browse path, too (although it shortens the browse path by removing namespaces, but that may work in your case).

Best regards
Z.

Best regards
Last edit: 05 May 2020 04:56 by support.

Please Log in or Create an account to join the conversation.

More
04 May 2020 14:21 #8458 by cadomanis
Thank you for your reply. Perhaps a concrete example would be the most helpful and efficient way to look at this for me. Below is an example of me trying to call a method. While I have no problem doing this with the NodeId strings, I cannot seem to make this work with my different attempts at Browse Paths. Perhaps you could illustrate this with a working approach with browse paths using some of the methods you listed above?
        {
 
            var endpointDescriptor = new UAEndpointDescriptor("opc.tcp://192.168.1.20:4840");
 
 
 
            object[] inputs =
            {
                double.Parse(textBoxSetPos.Text), 
                0
            };
 
            TypeCode[] typeCodes =
            {
                TypeCode.Double,
                TypeCode.UInt16
            };
 
            var browsePathParser = new UABrowsePathParser { DefaultNamespaceUriString = "urn:BeckhoffAutomation:Ua:PLC1" };
 
            // Prepare arguments
 
            var methodCallArguments = new[]
            {
                new UACallArguments(endpointDescriptor,
                    "ns=4;s=OpcCommunication.OpcEffect",
                    //browsePathParser.Parse("[ObjectsFolder]/PLC1/OpcCommunication/OpcEffectLinear"),
                    //new UANodeDescriptor
                    //{
                    //    BrowsePath = UABrowsePath.Parse("[ObjectsFolder]/PLC1/OpcCommunication/OpcEffectLinear","urn:BeckhoffAutomation:Ua:PLC1")
                    //},
                    "ns=4;s=OpcCommunication.OpcEffect#SetPosition",
                    //browsePathParser.Parse("[ObjectsFolder]/PLC1/OpcCommunication/OpcEffectLinear#SetPosition"),
 
                    inputs,
                    typeCodes)
            };
 
            // Instantiate the client object
            var client = new EasyUAClient();
 
            // Perform the operation
            object[] outputs;
            try
            {
                outputs = client.CallMultipleMethods(methodCallArguments);
 
            }
            catch (UAException uaException)
            {
                Console.WriteLine("*** Failure: {0}", uaException.GetBaseException().Message);
                return;
            }
 
            // Display results
            for (int i = 0; i < outputs.Length; i++)
                Console.WriteLine("outputs[{0}]: {1}", i, outputs[i]);
        }

As always, thanks for the assistance.

Please Log in or Create an account to join the conversation.

More
04 May 2020 13:18 #8457 by cadomanis
Thank you for the detailed output. I see the error I was making. Thanks for testing.

Please Log in or Create an account to join the conversation.

More
03 May 2020 07:44 #8455 by support
Hello,
Re

I am wondering if there are any more complex examples you might have to share on mapping and the detailed development of decorating classes with Attributes, or build the mapping programmatically as you suggest. I have tried to study and work through the examples in the documentation, but they do not seem to use any of the more complex techniques you refer to.


There aren't more examples than that what is referred to in Live Mapping documentation. I am ready to help you out with questions you may have, and provide examples where needed.

The other little pieces that I mentioned weren't really that "advanced". One of them was that while the "root" node of the mapping (where you start from) might be defined by an attribute (using the NodeId parameter in [UANode]) - making it fixed once and for all, it might alternatively be defined from the code. This approach is, however, already the one demonstrated in the provided example - it is done using the "mapping context" object passed to the map call, as below. Note that in this example the starting node is also defined using browse path, simple because the sample server uses numeric Node Ids and they are not very illustrative, such as "i=12345" (which nicely shows the difference between what node Ids and browse paths are for).
mapper.Map(boiler1, new UAMappingContext
            {
                EndpointDescriptor = endpointDescriptor,   
                // The NodeDescriptor below determines where in the OPC address space we want to map our data to.
                NodeDescriptor = new UANodeDescriptor
                    {
                        // '#' is a reserved character in a browse name, and must be escaped by '&' in the path below.
                        BrowsePath = UABrowsePath.Parse("[ObjectsFolder]/Boilers/Boiler &#1", "http://opcfoundation.org/UA/Boiler/")
                    },
                MonitoringParameters = 1000,  // requested sampling interval (for subscriptions)
            });
 

The other thing I mentioned was the fact that you do not need the escape the '' if you do not parse the browse paths form a string, but rather construct it from its individual elements. This does not really relate to Live Mapping directly; it is meant for the approach where you are writing the code for constructing the browse paths.So, instead of parsing, you might create UABrowseNode with a given starting node, and then use AppendElement method to append at its end. For the AppendElement, yoiu need UABrowsePathElement - but when constructing it, you would not do escaping in its name. The escaping is only for parsing the whole browse path - because there are delimiters (such as '.', '/', '[', ...) that need to be recognized in its syntax.

Regards

Please Log in or Create an account to join the conversation.

More
03 May 2020 06:14 #8454 by support
Hello,

The NodeID (that you have pointed to in the photo) has nothing to do (in general case, and in this case as well) with how the correct browse path for that node needs to be formed. The Node IDs can be even randomly generated. That's the whole purpose of browse paths - to allow meaningful navigation - which Node IDs by itself do not provide.

The browse path I have suggested does work, with your server. I have tested it before I have given it to you. Here is my code:
static void Main(string[] args)
        {
            //UAEndpointDescriptor endpointDescriptor = "opc.tcp://192.168.1.20:4840";
            UAEndpointDescriptor endpointDescriptor = "opc.tcp://67.<REMOVED>:4840";
 
            // Instantiate the client object
            var client = new EasyUAClient();
 
            var browsePathParser = new UABrowsePathParser { DefaultNamespaceUriString = "urn:BeckhoffAutomation:Ua:PLC1" };
 
            // Prepare arguments
            // Note: Add error handling around the following statement if the browse paths are not guaranteed to be
            // syntactically valid.
            var readArgumentsArray = new[]
            {
                new UAReadArguments(endpointDescriptor, 
                    browsePathParser.Parse("[ObjectsFolder]/PLC1/OpcCommunication/OpcEffectLinear.OpcEffectLinear&[1&].stStatus")),
                    //"nsu=urn:BeckhoffAutomation:Ua:PLC1;ns=4;s=OpcCommunication.OpcEffectLinear[1].stStatus"),
                new UAReadArguments(endpointDescriptor,
                    browsePathParser.Parse("[ObjectsFolder]/PLC1/OpcCommunication.OpcEffect.stStatus")),
                new UAReadArguments(endpointDescriptor,
                    browsePathParser.Parse("[ObjectsFolder]/PLC1/OpcCommunication.OpcTestData1")),
 
            };
 
            // Obtain attribute data.
            UAAttributeDataResult[] resultArray = client.ReadMultiple(readArgumentsArray);
 
            // Display results
            for (int i = 0; i < resultArray.Length; i++)
            {
                UAAttributeDataResult attributeDataResult = resultArray[i];
                if (attributeDataResult.Succeeded)
                    Console.WriteLine($"results[{i}].AttributeData: {attributeDataResult.AttributeData}");
                else
                    Console.WriteLine($"results[{i}]: *** Failure: {attributeDataResult.ErrorMessageBrief}");
            }
        }

And here is its output:
results[0].AttributeData: (nsu=urn:BeckhoffAutomation:Ua:PLC1 ;ns=4;s=ST_OpcStatusEffect) structured {OpcLabs.EasyOpc.UA.ComplexData.UAGenericObject} @2020-05-03T01:12:04.037 @@2020-05-03T01:12:04.037; Good
results[1].AttributeData: (nsu=urn:BeckhoffAutomation:Ua:PLC1 ;ns=4;s=ST_OpcStatusEffect) structured {OpcLabs.EasyOpc.UA.ComplexData.UAGenericObject} @2020-05-03T01:12:04.037 @@2020-05-03T01:12:04.037; Good
results[2].AttributeData: (nsu=urn:BeckhoffAutomation:Ua:PLC1 ;ns=4;s=ST_OpcMoveAbsoluteData) structured {OpcLabs.EasyOpc.UA.ComplexData.UAGenericObject} @2020-05-03T01:12:04.037 @@2020-05-03T01:12:04.037; Good
 

I will reply to the remainder of your post separately later.

Best regards

Please Log in or Create an account to join the conversation.

More
02 May 2020 21:53 #8452 by cadomanis
Again, thanks for all the assistance.

I had actually tried the path as you indicated, but this is in fact why I added the photo because it still is not working.

If you look at the right side of the photo, I have selected that node, and in its NodeID it doesn't in fact show that other level of nesting as indicated in the path you suggested.

I ran the test again today, and I am still unable to read that variable with either:
"[ObjectsFolder]/PLC1/OpcCommunication.OpcEffectLinear.OpcEffectLinear&[1&].stStatus"

or

"[ObjectsFolder]/PLC1/OpcCommunication.OpcEffectLinear&[1&].stStatus"

Wondering if you have any further thoughts?

Additionally, thanks for the general feedback/comments on mapping. I am wondering if there are any more complex examples you might have to share on mapping and the detailed development of decorating classes with Attributes, or build the mapping programmatically as you suggest. I have tried to study and work through the examples in the documentation, but they do not seem to use any of the more complex techniques you refer to.

Please Log in or Create an account to join the conversation.

Moderators: support
Time to create page: 0.097 seconds