locka99 / opcua

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

How to connect to a pre configured endpoint #17

Open crenwick opened 5 years ago

crenwick commented 5 years ago

Hello!

Not sure if I'm using this wrong but when I try to create a session from a client I cannot get the program to connect to a custom endpoint I configured. Instead, it only allows me to connect to an endpoint that the server responds with.

For example, if I create a session via the ClientBuilder API like so:

let builder = ClientBuilder::new()
    .endpoint(
       "lab_plc",
       ClientEndpoint {
           url: String::from("opc.tcp://192.168.0.1:4840"),
           security_policy: String::from(SecurityPolicy::None.to_str()),
           security_mode: String::from(MessageSecurityMode::None),
           user_token_id: String::from("ANONYMOUS")
        },
    )
    ...

let mut client = builder.client().unwrap();
let session = client.connect_and_activate(Some("lab_plc")).unwrap();

the connect_and_activate method will not use the "lab_plc" endpoint like I expected it to. Instead that method calls Client.get_server_endpoints() to build an internal list of endpoints that it gets from asking the OPCUA server what endpoints exist. If the server doesn't respond with the endpoint that I want (for example, instead of listing opc.tcp://192.168.0.1:4840 as an endpoint url, the server responds with a hostname like opc.tcp://automation:4840 that doesn't resolve correctly on the network) then there is no alternative way for me to override that hostname with my custom url.

My "fix" was to clone the types crate and edit the endpoint_description.rs file so that the decode method returned a hardcoded value in the struct:

        Ok(EndpointDescription {
            // endpoint_url,
            endpoint_url: UAString::from("opc.tcp://192.168.0.1:4840"),
            server,
            server_certificate,
            security_mode,
            security_policy_uri,
            user_identity_tokens,
            transport_profile_uri,
            security_level,
        })

Am I using this API wrong? Is there a better API to use where I can send it the endpoint url I want?

Thanks!

locka99 commented 5 years ago

Hi, I agree the client API is a little confusing.

The current default behaviour for the client is that you define the endpoints you expect to be on the server, including one which is your default. Then you call connect_and_activate() which will attempt to connect to the endpoint you specify by its id or with the default. But if the endpoint doesn't match any on the server you'll get an error.

If you prefer to blindly connect, you should be able to do something like this pseudo code instead:

let mut client = ClientBuilder::new()
    .endpoint(
       "lab_plc",
       ClientEndpoint {
           url: String::from("opc.tcp://192.168.0.1:4840"),
           security_policy: String::from(SecurityPolicy::None.to_str()),
           security_mode: String::from(MessageSecurityMode::None),
           user_token_id: String::from("ANONYMOUS")
        },
    )
    .default_endpoint("lab_plc")
    .client().unwrap();
// This will connect to the default endpoint "lab_plc" and get endpoints
let endpoints = client.get_server_endpoints();
// This will try for an exact match and then a fuzzy match on the server's endpoints vs your input criteria
if let Some(endpoint) =client.find_server_endpoint(&endpoints, my_url, my_policy, my_mode) {
  let mut session = client.new_session_from_endpoint(&endpoint).unwrap();
  session.connect_and_activate_session();
  //...
}

This tends to be a problem with OPC UA. Servers may be configured to advertise endpoints that do not match the hostname of the computer, or there may be multiple hostnames. Thus far I've tried to code the client to be as flexible as possible, but I think I will work on simplifying it further for the common cases.

locka99 commented 5 years ago

One further improvement is could probably do this (again pseudo code)

let mut client = ClientBuilder::new().client.unwrap();
let endpoints = client.get_server_endpoints_from_url(server_url);
//....

i.e. you don't need to supply an endpoint at all in your builder, but instead just call the ad hoc get_server_endpoints_from_url() and then match up.

crenwick commented 5 years ago

Hmmm. After changing the get_server_endpoints_from_url function to be public, the code paniced when calling read_nodes on the session after calling write.

Panics on this scope:

    let data_values = {
        let mut session = session.write().unwrap();
        session.read_nodes(&read_nodes)?.unwrap     // panics here
    };

While the same code doesn't panic when I use the method I described above (hard-coding the url in the decode impl of the struct, and calling connect_and_activate).

I didn't have time to investigate where in the library read_nodes is failing, but I hope to do that after the holidays.

Thanks for getting back to me!

locka99 commented 5 years ago

No probs & enjoy your holidays! When you're back, let me have a sample of what you're doing and I will see if I can identify where the issue lies.

crenwick commented 5 years ago

Hey,

Got to play around with the code a bit again today. I found a way around the API, but its a little hacky. Please let me know if there is a more straightforward API I should be using.

After creating client endpoint, I created a client:

    let mut client = ClientBuilder::new()
        ...
        .endpoint("lab", client_endpoint.clone())
        .default_endpoint("lab")
        .client()
        .unwrap();

I then created a custom endpoint by cloning one of the server-returned endpoints and overriding the url:

    let server_eps = client.get_server_endpoints_from_url(SERVER_URL).unwrap();
    let mut ep = sample_eps[0].clone();
    ep.endpoint_url = UAString::from(SERVER_URL);
    let eps = vec![ep];

Then I could create a session using the new endpoint vector:

    let session = client
        .new_session_from_endpoint(&client_endpoint, &eps)
        .unwrap();

Finally I was able to call write(), connect(), create_session(), activate_session(), and read_nodes(&nodes) on that session to get the values I wanted.

That said, I still had to make the get_server_endpoints_from_url function public, as I found it the most straightforward way to create an valid EndpointDescription.