locka99 / opcua

A client and server implementation of the OPC UA specification written in Rust
Mozilla Public License 2.0
501 stars 131 forks source link

[Question] Generating structs from XML for clients #153

Closed Pascal-So closed 2 years ago

Pascal-So commented 2 years ago

disclaimer: I'm not too familiar with the whole information modelling / nodeset2 workflow so please feel free to point out if you think I might be misunderstanding something.

Assuming I am given the nodeset2 xml, bsd, csv, etc. files, my understanding is that these files can be used to set up object types, object instances, and further nodes on the server, and can also be used to inform a client about these nodes.

Thanks to the Readme for the schema tools I was able to take a nodeset2.xml and generate a rust module with which a server address space can be populated.

My question is now, what would the corresponding step for clients look like? I found the gen_types.js which takes a .bsd file and generates rust structs along with impl BinaryEncoder which is what I want, but the script doesn't look like it's intended to be modified to generate other types than the default types defined by the OPC UA Spec.

If my goal is to interact with a server that uses namespaces for which I have the nodeset2 files (e.g. one of the EUROMAP standards), what's the intended way to get this information into my code, preferably in a way to have as much information as possible already present at compile time instead of having to get information from the server at runtime?

Btw thank you for working on this library, so far this looks very pleasant to use overall!

schroeder- commented 2 years ago

Currently it's not possible to generate from .bsd files. To do this we need to modify gen_types.js to allow extern .bsd files and change the outputs. Maybe i can have a look into it.

locka99 commented 2 years ago

The nodeset schema files aren't really describing structs, they're describing nodes in the address space - objects, variables, data types, methods and the relationships (references) between each other, e.g. that a variable is a child of an object and so on.

There are enums & structs in the .bsd files which are turned into generated types so they can be serialized into and out of extension objects. Most of the fundamental types are handwritten and only the types / enums that are composites of fundamental types are machine generated. I'm not sure if it is .bsd types you are after or the nodeset?

At present the node set compiler in the project is mostly about populating the address space at startup and doesn't really help much beyond that. I guess the code generators could be extended in useful ways, e.g. bespoke builders to assist servers to make instances of objects and so on.

From the client side, it might be possible to create helpers that allow someone to monitor all the vars of an object in one go (for example) but it wouldn't be a structure, more of just a helper to get all the stuff in one subscription. If you have an example of a commercial or open source OPC UA library that does generate code in a useful way I can take a look at it.

Pascal-So commented 2 years ago

I should have maybe mentioned more details about my specific use case and not framed it as such a general question. I'm specifically trying to send jobs to a server that uses EUROMAP 83 and provides a SendJobList method. The input arguments to this method contain a list of extension objects. See the relevant snippet here:

The method:

<UAMethod ParentNodeId="ns=1;i=1032" NodeId="ns=1;i=7036" BrowseName="1:SendJobList">
    <DisplayName>SendJobList</DisplayName>
    <Description>Sends a list of jobs available on the client to the server</Description>
    <References>
        <Reference ReferenceType="HasProperty">ns=1;i=6384</Reference>
        <Reference ReferenceType="HasModellingRule">i=80</Reference>
        <Reference ReferenceType="HasComponent" IsForward="false">ns=1;i=1032</Reference>
    </References>
</UAMethod>
<UAVariable DataType="Argument" ParentNodeId="ns=1;i=7036" ValueRank="1" NodeId="ns=1;i=6384" ArrayDimensions="1" BrowseName="InputArguments">
    <DisplayName>InputArguments</DisplayName>
    <References>
        <Reference ReferenceType="HasModellingRule">i=78</Reference>
        <Reference ReferenceType="HasTypeDefinition">i=68</Reference>
        <Reference ReferenceType="HasProperty" IsForward="false">ns=1;i=7036</Reference>
    </References>
    <Value>
        <uax:ListOfExtensionObject>
            <uax:ExtensionObject>
                <uax:TypeId>
                    <uax:Identifier>i=297</uax:Identifier>
                </uax:TypeId>
                <uax:Body>
                    <uax:Argument>
                        <uax:Name>JobList</uax:Name>
                        <uax:DataType>
                            <uax:Identifier>ns=1;i=3021</uax:Identifier>
                        </uax:DataType>
                        <uax:ValueRank>1</uax:ValueRank>
                        <uax:ArrayDimensions/>
                        <uax:Description/>
                    </uax:Argument>
                </uax:Body>
            </uax:ExtensionObject>
        </uax:ListOfExtensionObject>
    </Value>
</UAVariable>

The data type:

<UADataType NodeId="ns=1;i=3021" BrowseName="1:JobListElementType">
    <DisplayName>JobListElementType</DisplayName>
    <Description>Description of a job in a job list</Description>
    <References>
        <Reference ReferenceType="HasEncoding">ns=1;i=5036</Reference>
        <Reference ReferenceType="HasEncoding">ns=1;i=5038</Reference>
        <Reference ReferenceType="HasSubtype" IsForward="false">i=22</Reference>
    </References>
    <Definition Name="1:JobListElementType">
        <Field DataType="String" Name="JobName">
            <Description>Name of the job</Description>
        </Field>
        <Field DataType="String" Name="JobDescription">
            <Description>Description of the job</Description>
        </Field>
        <Field DataType="String" Name="JobClassification">
            <Description>Classification of the job</Description>
        </Field>
        <Field DataType="String" Name="CustomerName">
            <Description>Name of the cumstomer for that the job is produced</Description>
        </Field>
        <Field DataType="String" Name="ProductionDatasetName">
            <Description>Name of the production dataset which is needed for the job</Description>
        </Field>
        <Field DataType="String" Name="ProductionDatasetDescription">
            <Description>Additional description of the production dataset which is needed for the job</Description>
        </Field>
        <Field DataType="String" ValueRank="1" ArrayDimensions="0" Name="Material">
            <Description>Array of material names used for the job</Description>
        </Field>
        <Field DataType="String" ValueRank="1" ArrayDimensions="0" Name="ProductName">
            <Description>Array of product names produced by the job</Description>
        </Field>
        <Field DataType="String" ValueRank="1" ArrayDimensions="0" Name="ProductDescription">
            <Description>Array of descriptions of the products produced by the job</Description>
        </Field>
        <Field DataType="String" Name="JobPriority">
            <Description>Priority of the job</Description>
        </Field>
        <Field DataType="DateTime" Name="PlannedStart">
            <Description>Planned start of the job</Description>
        </Field>
        <Field DataType="Duration" Name="PlannedProductionTime">
            <Description>Planned production time</Description>
        </Field>
        <Field DataType="DateTime" Name="LatestEnd">
            <Description>Latest end of the job</Description>
        </Field>
    </Definition>
</UADataType>

If the method was designed to only accept one job at a time then it would probably have been designed as a method that takes multiple primitives, but given that it's a list of these structs I can understand why they chose to use extension objects for this purpose.

In order to call this method from a client written using your library, the way I understand it, I have to write a struct and implement the BinaryEncoder trait and then when calling the method I probably have to create a CallMethodRequest with a Variant::Array of Variant::ExtensionObject containing ExtensionObject::from_encodable, is this correct?

I'm not sure if it is .bsd types you are after or the nodeset?

With the disclaimer that I'm not too sure on where the distinction between the .bsd and the .xml lies, the main benefits of code generation in my view would probably be to provide structs and impls for working with extension objects. I guess I'm also a bit unsure if extension objects are sort of the exception case and usually people work mostly with interfaces that only combine primitive types directly?

If you have an example of a commercial or open source OPC UA library that does generate code in a useful way I can take a look at it.

The closest thing I can think of would be the the load_type_definitions() function in the python-opcua library, link here. After calling this in a client, one can later create extension objects by calling get_ua_class('custom-data-type-name')() and then assigning the attributes on the returned object. This python version gets all the type information from the server at runtime, but I'm imagining something similar to be possible in rust by having the nodeset information available at compile time or when calling a script beforehand.

Does that sound 1. reasonable as a goal and 2. within the scope of where you want this library to go?

Pascal-So commented 2 years ago

Oh, just realized you already wrote a PR for extending the js script functionality, thank you for looking into this so quickly! I'll play around with this and tell you if this addresses my usecase.

locka99 commented 2 years ago

I've merged the PR so if it solves your immediate concern you can keep the issue closed