awcullen / opcua

OPC Unified Architecture (OPC UA) in Go.
MIT License
79 stars 18 forks source link

Question: Attribute not supported when reading description of an object #10

Closed rjboer closed 1 year ago

rjboer commented 1 year ago

When trying to read the discription of an object,
I get the following response: The attribute is not supported for the specified Node.

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, }, }, }

image

rjboer commented 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: scanmask: 13, statuscode:The operation completed successfully., value: scanmask: 14, statuscode:The operation completed successfully., value:ns=4;i=467 scanmask: 15, statuscode:The operation completed successfully., value:-1 scanmask: 16, statuscode:The attribute is not supported for the specified Node., value: scanmask: 17, statuscode:The operation completed successfully., value:3 scanmask: 18, statuscode:The operation completed successfully., value:3 scanmask: 19, statuscode:The operation completed successfully., value:-1 scanmask: 20, statuscode:The operation completed successfully., value:false scanmask: 21-25, statuscode:The attribute is not supported for the specified Node., value: scanmask: 26, statuscode:The attribute is not supported for the specified Node., value:0 <--- weird, this gives not a nil like the others scanmask: 27, statuscode:The operation completed successfully., value:3

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: scanmask: 6, statuscode:The operation completed successfully., value:0 scanmask: 7, statuscode:The operation completed successfully., value:0 scanmask: 8, statuscode:The operation completed successfully., value:false scanmask: 9 - 22, statuscode:The attribute is not supported for the specified Node., value: scanmask: 23, statuscode:The operation completed successfully., value:{ns=4;i=468 ns=3;i=3400 Structure [{major i=3 -1 [] 0 false} {minor i=3 -1 [] 0 false} {patch i=3 -1 [] 0 false}]} scanmask: 24-25, statuscode:The attribute is not supported for the specified Node., value: scanmask: 26, statuscode:The attribute is not supported for the specified Node., value:0 scanmask: 27, statuscode:The attribute is not supported for the specified Node., value:

awcullen commented 1 year ago

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 {
mark-resato commented 1 year ago

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.

image

awcullen commented 1 year ago

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

image

mark-resato commented 1 year ago

i downloaded uaExpert and found the DataTypeDefinition

image

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

image

But the output i get is still

image

awcullen commented 1 year ago

Please reveal the ExtensionObject's TypeID from the WireShark message. It should match the DefaultEncodingId "ns=4;i=13"

awcullen commented 1 year ago

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!

awcullen commented 1 year ago

Fixed bug in type registry lookup. Added Example.

mark-resato commented 1 year ago

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

image

rjboer commented 1 year ago

server sends Hi Andrew,

Have you tried this with a Siemens PLC?

awcullen commented 1 year ago

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"`))
}
rjboer commented 1 year ago

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

rjboer commented 1 year ago

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