Closed rjboer closed 1 year ago
Got this figured out.... It's an siemens UDT (OPC complex type)
Question then is: How can I read a Complex type? Can I read it as a whole or: how do I get the id's of the sub elements of the struct?
I wrote handle to scan the attributes of a node.
AttributeIDNodeID uint32 = 1 AttributeIDNodeClass uint32 = 2 AttributeIDBrowseName uint32 = 3 AttributeIDDisplayName uint32 = 4 AttributeIDDescription uint32 = 5 AttributeIDWriteMask uint32 = 6 AttributeIDUserWriteMask uint32 = 7 AttributeIDIsAbstract uint32 = 8 AttributeIDSymmetric uint32 = 9 AttributeIDInverseName uint32 = 10 AttributeIDContainsNoLoops uint32 = 11 AttributeIDEventNotifier uint32 = 12 AttributeIDValue uint32 = 13 AttributeIDDataType uint32 = 14 AttributeIDValueRank uint32 = 15 AttributeIDArrayDimensions uint32 = 16 AttributeIDAccessLevel uint32 = 17 AttributeIDUserAccessLevel uint32 = 18 AttributeIDMinimumSamplingInterval uint32 = 19 AttributeIDHistorizing uint32 = 20 AttributeIDExecutable uint32 = 21 AttributeIDUserExecutable uint32 = 22 AttributeIDDataTypeDefinition uint32 = 23 AttributeIDRolePermissions uint32 = 24 AttributeIDUserRolePermissions uint32 = 25 AttributeIDAccessRestrictions uint32 = 26 AttributeIDAccessLevelEx uint32 = 27
The discription of the node was:
Datatype scan of node: ns=4;i=473
scanmask: 1, statuscode:The operation completed successfully., value:ns=4;i=473
scanmask: 2, statuscode:The operation completed successfully., value:2
scanmask: 3, statuscode:The operation completed successfully., value:4:ElectricalVersion
scanmask: 4, statuscode:The operation completed successfully., value:ElectricalVersion
scanmask: 5, statuscode:The operation completed successfully., value:Module electrical version
scanmask: 6, statuscode:The operation completed successfully., value:0
scanmask: 7, statuscode:The operation completed successfully., value:0
scanmask: 8 -12, statuscode:The attribute is not supported for the specified Node., value:
The data type turned out to be: ns=4;i=467 The defenition of the type was as follows:
Datatype scan of node: ns=4;i=467
scanmask: 1, statuscode:The operation completed successfully., value:ns=4;i=467
scanmask: 2, statuscode:The operation completed successfully., value:64
scanmask: 3, statuscode:The operation completed successfully., value:4:UDT_SemVer
scanmask: 4, statuscode:The operation completed successfully., value:UDT_SemVer
scanmask: 5, statuscode:The attribute is not supported for the specified Node., value:
Hi @rjboer, thanks for trying the library.
You found that nodes have NodeClass attribute, and if NodeClass is Variable, the node will have Value, DataType, and ValueRank attributes. You found the DataType can be a 'built-in' type or complex structure. ValueRank can tell you if it is a slice.
Sometime the UA server will describe the structure in the DataTypeDefinition attribute. This library does not yet have a feature to use that definition.
Sometimes the UA server will provide NodeIDs for the components of the structure so that you can read the component parts. If the components are 'built-in' types, then this may be sufficient for you.
You can also try to create your own public struct and register it with the library. For instance:
type SemVerDataType struct {
Major uint8
Minor uint8
Patch uint8
}
func init() {
ua.RegisterBinaryEncodingID(reflect.TypeOf(SemVerDataType{}), ua.NewExpandedNodeID(ua.ParseNodeID("ns=4;i=468")))
}
Then when you read a variable of the 'SemVer' datatype, the library will decode it. You just have to cast the Value to your type:
if semVer, ok := res.Results[0].Value.(SemVerDataType); ok {
i tried something similar to rjboer, with the following code
package main
import (
"context"
"fmt"
"github.com/awcullen/opcua/client"
"github.com/awcullen/opcua/ua"
"reflect"
)
type CustomStruct struct {
W1 uint16
W2 uint16
}
func init() {
ua.RegisterBinaryEncodingID(reflect.TypeOf(CustomStruct{}), ua.NewExpandedNodeID(ua.ParseNodeID("ns=4;i=14")))
}
func main() {
ctx := context.Background()
ch, err := client.Dial(
ctx,
"opc.tcp://50.50.50.1:4840",
client.WithInsecureSkipVerify(), // skips verification of server certificate
)
if err != nil {
fmt.Printf("Error opening client connection. %s\n", err.Error())
return
}
// read single data value
readRequest := &ua.ReadRequest{
NodesToRead: []ua.ReadValueID{
{
NodeID: ua.ParseNodeID(fmt.Sprintf("ns=4;i=14")),
AttributeID: ua.AttributeIDValue,
},
},
}
readResponse, err := ch.Read(ctx, readRequest)
if err != nil {
fmt.Println("read error:", err)
return
}
// print the read result(s)
for _, v := range readResponse.Results {
fmt.Println(v)
fmt.Printf("Type: %T, value: %v, %v \n", v.Value, v.Value, v.StatusCode)
}
if semVer, ok := readResponse.Results[0].Value.(CustomStruct); ok {
fmt.Println(semVer)
} else {
fmt.Println("result is not of custom type")
}
}
when i run this i get the following result
{<nil> The operation completed successfully. 2023-01-06 08:05:14.3210717 +0000 UTC 0 0001-01-01 00:00:00 +0000 UTC 0}
Type: <nil>, value: <nil>, The operation completed successfully.
result is not of custom type
i used wireshark to verify that the server is sending the correct response, but the results remain empty after which i tried the same read in cpp and got the following result.
I forgot to mention an important point! When you register the custom type with the library, use the NodeID that corresponds to the BinaryEncodingID of the type. This will be different than the NodeID of the variable or even the NodeID of the DataType.
With UAExpert, you can find the BinaryEncodingID by opening DataTypes...BaseDataType...Structures...[name of type]
Then find the DataTypeDefinition...DefaultEncodingID
i downloaded uaExpert and found the DataTypeDefinition
Then i changed the nodeID in the init function to the following
func init() {
ua.RegisterBinaryEncodingID(
reflect.TypeOf(CustomStruct{}),
ua.NewExpandedNodeID(ua.ParseNodeID("ns=4;i=13")),
)
}
wireshark shows that the server is returning the data
But the output i get is still
Please reveal the ExtensionObject's TypeID from the WireShark message. It should match the DefaultEncodingId "ns=4;i=13"
I forgot the second rule: custom types need to be registered using the Namespace URI.
func init() {
ua.RegisterBinaryEncodingID(reflect.TypeOf(CustomStruct{}), ua.ParseExpandedNodeID("nsu=<your custom type's namespace uri>;i=14"))
}
I really need to add some tests and examples for this!
Fixed bug in type registry lookup. Added Example.
Please reveal the ExtensionObject's TypeID from the WireShark message. It should match the DefaultEncodingId "ns=4;i=13"
this is the response the opcua server sends
server sends Hi Andrew,
Have you tried this with a Siemens PLC?
Here is a program to read a custom structure from a S7-1500. Please get v1.0.0 or later source using: go get github.com/awcullen/opcua@v1.0.0
package main
import (
"context"
"fmt"
"reflect"
"github.com/awcullen/opcua/client"
"github.com/awcullen/opcua/ua"
)
// This example demonstrates reading a variable with type of Simatic structure.
func main() {
ctx := context.Background()
// open a connection to opcua server of running S7-1500 PLC on local network.
ch, err := client.Dial(
ctx,
"opc.tcp://192.168.254.44:4840",
client.WithInsecureSkipVerify(), // skips verification of server certificate
)
if err != nil {
fmt.Printf("Error opening client connection. %s\n", err.Error())
return
}
// prepare read request
req := &ua.ReadRequest{
NodesToRead: []ua.ReadValueID{
{
// use backticks `` for Simatic nodeIDs with embedded quotes
NodeID: ua.ParseNodeID(`ns=3;s="Data_block_1"."PointA"`),
AttributeID: ua.AttributeIDValue,
},
},
}
// send request to server. receive response or error
res, err := ch.Read(ctx, req)
if err != nil {
fmt.Printf("Error reading TE_Vector. %s\n", err.Error())
ch.Abort(ctx)
return
}
// print results
if custom, ok := res.Results[0].Value.(TE_Vector); ok {
fmt.Printf("PointA:\n")
fmt.Printf(" X: %f\n", custom.X)
fmt.Printf(" Y: %f\n", custom.Y)
fmt.Printf(" Z: %f\n", custom.Z)
} else {
fmt.Println("Error decoding TE_Vector.")
}
// close connection
err = ch.Close(ctx)
if err != nil {
ch.Abort(ctx)
return
}
// Output:
// PointA:
// X: 1.000000
// Y: 2.000000
// Z: 3.000000
}
type TE_Vector struct {
X float32
Y float32
Z float32
}
func init() {
// use backticks `` for Simatic nodeIDs with embedded quotes
ua.RegisterBinaryEncodingID(reflect.TypeOf(TE_Vector{}), ua.ParseExpandedNodeID(`nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_"Vector"`))
}
Hi Andrew, Thanks for checking! This is indeed similar to our attempt We have a case though where it doesn't work, we'll attach the tia portal project
OK...we fixed it all! For future reference...: The thing is that the type definition is not similar to the shown in tools like UA expert... or through the browse function... So far I have seen TD_"UDTsemver", TA"UDT_semver" and "UDTsemver" Through checks in the wireshark response it apperently is TE"UDT_semver" <- which is weird, but works (and honestly just as Andrew explained above).
Will figure out why the browse gave a wrong type def.
Furthermore Objects don't have values!, Variables are structs that can be queried... so you need to nest your UDT!! In OPC UA, the node class "Object" represents a type of node that can have child nodes, properties, and methods. While the "Object" node itself does not have a value that can be read directly, you can still access its child nodes, properties, or methods, depending on their individual node classes and attributes
When trying to read the discription of an object, The attribute is not supported for the specified Node.
I get the following response:
The snippet works for other nodes (variables) It also doesn't work when refering to the the object through its identifier
req := &ua.ReadRequest{ NodesToRead: []ua.ReadValueID{ { NodeID: ua.ParseNodeID("ns=4;s=IdentificationData"), AttributeID: ua.AttributeIDDescription, }, }, }