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.
- Forum
- Discussions
- QuickOPC-UA in .NET
- Live Binding, Live Mapping
- AMapping ( and Reading) objects within arrays using BrowsePaths
AMapping ( and Reading) objects within arrays using BrowsePaths
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.
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.
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.
Please Log in or Create an account to join the conversation.
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
urn:BeckhoffAutomation:Ua:PLC1
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.
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
Please Log in or Create an account to join the conversation.
{
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.
Please Log in or Create an account to join the conversation.
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 ", "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.
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.
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.
- Forum
- Discussions
- QuickOPC-UA in .NET
- Live Binding, Live Mapping
- AMapping ( and Reading) objects within arrays using BrowsePaths