Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Method output arguments return <nil> Value() for ExtensionObject arrays representing custom structure dataTypes #768

Open
hbrackel opened this issue Feb 11, 2025 · 12 comments · May be fixed by #770

Comments

@hbrackel
Copy link

When calling a method whose output argument[0] is an array of custom structure dataType values, the corresponding output argument returns a slice of []*ua.ExtensionObject, where the ua.ExtensionObject.Value() == nil for each slice element. The same method called from e.g. UaExpert returns the correct decoded custom structure; that is the method returns the correct []*ua.ExtensionObject. Tested with multiple clients on multiple custom OPC UA servers. As a result, the ExtensionObject value cannot be decoded.

pseudo code:

...
oID := objectNodeId
mID := methodNodeId // method with zero input arguments; one output argument
req := &ua.CallMethodRequest{
		ObjectID:       oID,
		MethodID:       mID,
		InputArguments: []*ua.Variant{},
	}

resp, err := c.Call(ctx, req)
if err != nil {
	log.Fatal(err)
}
if got, want := resp.StatusCode, ua.StatusOK; got != want {
	log.Fatalf("got status %v want %v", got, want)
}
out := resp.OutputArguments[0].Value() 
// first output argument shall be an array of values of a custom structure dataType; 
// thus encoded as extensionObjects

extObjects, ok := out.([]*ua.ExtensionObject) // works up to here; 
// ...error handling omitted
for _, extObj range extObjects {
  value := extObj.Value(). // value is always nil??
}

@magiconair
Copy link
Member

Just to clarify:

  • The method is returning ONE value
  • The value is an array of extension objects

correct?

@hbrackel
Copy link
Author

in this concrete case, the method returns a single output argument, where the arg's variant is expected to contain an array of ExtensionObjects (encoded structured dataType values).

So: correct

magiconair added a commit that referenced this issue Feb 11, 2025
Call method without input args and return a single value with an
array of extension objects.

Fixes #768
@magiconair
Copy link
Member

magiconair commented Feb 11, 2025

Thank you. @hbrackel could you have a look at the test I've added in #770?

Is this a correct description of your problem?

@hbrackel
Copy link
Author

hbrackel commented Feb 11, 2025

gopcua-issue-768.xml.zip

@magiconair the signatures of the test are correct, but the output arguments would best be encoded custom OPC UA structure dataType values, while the test uses go structure types.
For a 100% reproduction of the problem, the server would define a custom OPC UA structure dataType (none of the OPC UA predefined structures) and return an array of values of that structured OPC UA dataType.

I don't know whether the problem is specifically about receiving encoded custom structure ExtensionObjects or just "any" ExtensionObjects.

I created a tiny OPC UA nodeset.xml with a sample dataType ad a method (see attached). Maybe this helps.

Thanks for taking the time and effort!

@magiconair
Copy link
Member

I thought that the Complex data type is a custom data type encoded as an ExtensionObject but maybe I'm mistaken. I'll have a look at the nodeset.

class Complex(uatypes.FrozenClass):
    ua_types = [
        ('i', 'Int64'),
        ('j', 'Int64'),
    ]

    def __init__(self, i=0, j=0):
        self.i = i
        self.j = j
        self._freeze = True

    def __str__(self):
        return f'Complex(i:{self.i}, j:{self.j})'

    __repr__ = __str__
...

if __name__ == "__main__":
    server = Server()
    server.set_endpoint("opc.tcp://0.0.0.0:4840/")

    ns = server.register_namespace("http://gopcua.com/")
    uatypes.register_extension_object('Complex', ua.NodeId("ComplexType", ns), Complex)
...

@magiconair
Copy link
Member

Could you capture a tcpdump of the UAExpert method call and attach it here?

Maybe something like this:

tcpdump -n -i eth0 -s 1520 -w dump.pcap port 4840

@hbrackel
Copy link
Author

hbrackel commented Feb 12, 2025

Sorry for the confusion - the term "complex" was meant to indicate complex (non-trivial) data structures not a complex number. My bad. I'll see what I can do about the tcpdump.

@magiconair
Copy link
Member

I understood you to mean "complex" to be a struct type. The "Complex" data type in the test is just a complex number by accident.

Your structure is also a struct. The only difference is that you use 4 instead of 2 fields and the field data types are not just numbers. But strings are length encoded byte arrays, DateTime fields are 8 byte values (int64) and a NodeID is a built-in data type. So this should work exactly like the "Complex" type in the test.

    <UADataType BrowseName="1:Issue768DataType" NodeId="ns=1;i=3003">
        <DisplayName>Issue768DataType</DisplayName>
        <References>
            <Reference ReferenceType="HasSubtype" IsForward="false">i=22</Reference>
            <Reference ReferenceType="HasEncoding">ns=1;i=5001</Reference>
            <Reference ReferenceType="HasEncoding">ns=1;i=5002</Reference>
        </References>
        <Definition Name="1:Issue768DataType">
            <Field DataType="String" Name="Name"/>
            <Field DataType="NodeId" Name="NodeId"/>
            <Field DataType="DateTime" Name="LowerBound"/>
            <Field DataType="DateTime" Name="UpperBound"/>
        </Definition>
    </UADataType>
type Issue768DataType struct {
	Name       string
	NodeId     *ua.NodeID
	LowerBound time.Time
	UpperBound time.Time
}

@hbrackel
Copy link
Author

The other difference is that ComplexNumberType is an OPC UA defined type (namespace 0, known to gopcua, while the custom structure is not. I am not familiar enough with Python OPC UA to predict what the ua.Variant([Complex(1,2), Complex(3,4]) will be encoded to. (line 41 in your test)

@magiconair
Copy link
Member

OK. If you could send me the tcpdump file then I can see what the protocol looks on the wire and see why the code doesn't decode it into an array of extension objects with the Issue768DataType from your nodeset.

@hbrackel
Copy link
Author

It may take until next Tuesday, as I'll be traveling from tomorrow morning. But I will obtain the information.

@hbrackel
Copy link
Author

hbrackel commented Feb 13, 2025

I hope the attached *.pcab is what you are looking for. Given limited admin rights on my company notebook, I created a server from the nodeset on my personal macOS machine. The recording is from a method call of the issue768 method using ProsysOPC's "Browser" client. The call succeeded and returned a 2 element array of the properly decoded Issue768DataType values.

Obviously, the securityMode is None.

I also wrote a simple gopcua client to call the method (code below). The returned output arguments correctly return an []*ua.ExtensionObject, but the Value of the ExtensionObjects is nil.

func main() {
	endpoint := "opc.tcp://localhost:4840"
	ctx := context.Background()

	c, err := opcua.NewClient(endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
	if err != nil {
		log.Fatal(err)
	}
	if err = c.Connect(ctx); err != nil {
		log.Fatal(err)
	}
	defer c.Close(ctx)

	req := &ua.CallMethodRequest{
		ObjectID:       ua.NewNumericNodeID(0, 85),
		MethodID:       ua.NewNumericNodeID(2, 7001),
		InputArguments: []*ua.Variant{},
	}

	resp, err := c.Call(ctx, req)
	if err != nil {
		log.Fatal(err)
	}
	if got, want := resp.StatusCode, ua.StatusOK; got != want {
		log.Fatalf("got status %v want %v", got, want)
	}
	outValue, ok := resp.OutputArguments[0].Value().([]*ua.ExtensionObject)
	if !ok {
		log.Fatalf("Received unexpected type %T, want []*ua.ExtensionObject", outValue)
	}
	firstArrayElement := outValue[0]
	fmt.Printf("firstArrayElement.TypeId: %v, Value: %v\n", firstArrayElement.TypeID, firstArrayElement.Value)
	// prints 'firstArrayElement.TypeId: ns=2;i=5001, Value: <nil>'
}

dump.pcap.zip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants