convertersystems / opc-ua-client

Visualize and control your enterprise using OPC Unified Architecture (OPC UA) and Visual Studio.
MIT License
399 stars 116 forks source link

RemoteEndpoint is always the last one #27

Closed roozbeh63 closed 7 years ago

roozbeh63 commented 7 years ago

in OpenAsync function in UaTcpSessionClient class you are setting the RemoteEndpoint with this function:

this.RemoteEndpoint = getEndpointsResponse.Endpoints.OrderBy(e => e.SecurityLevel).Last()

which gives me the last endpoint (obviously). however for my application I need to use the endpoint with none security, which I am using in Xamarin, and I don't have access to change it. can you help me with that? thanks in advanced

awcullen commented 7 years ago

One way to do this is to turn off the secure endpoints at the server. :)

But maybe that's not practical. You can use the code below to get the available endpoints yourself. Then you can select the one you want.

            Console.WriteLine($"Discovering endpoints of '{this.endpointUrl}'.");
            var getEndpointsResponse = await UaTcpDiscoveryClient.GetEndpointsAsync(
                new GetEndpointsRequest
                {
                    EndpointUrl = this.endpointUrl,
                    ProfileUris = new[] { TransportProfileUris.UaTcpTransport }
                });
            var selectedEndpoint = getEndpointsResponse.Endpoints.First(e => e.SecurityMode == MessageSecurityMode.None);

            var session = new UaTcpSessionClient(
                this.localDescription,
                this.certificateStore,
                ed => Task.FromResult<IUserIdentity>(new UserNameIdentity("root", "secret")),
                selectedEndpoint,
                loggerFactory: this.loggerFactory);
roozbeh63 commented 7 years ago

thanks for the help, now i have another issue. when I read the node from the device it always gives me zero. here is the code that i have, maybe you can see the problem.

this is the code in ViewModel ` [Subscription(publishingInterval: 250, keepAliveCount: 20)] // Step 2: Add a [Subscription] attribute. public class MainPageViewModel : ViewModelBase { private readonly ILogger logger; private readonly UaTcpSessionClient session;

    public MainPageViewModel(UaTcpSessionClient session)
    {
        this.session = session;
        session?.Subscribe(this);
    }

    [MonitoredItem(nodeId: "ns=12;s=Motion.AxisSet.LocalControl.Axis1.State")] // Step 4: Add a [MonitoredItem] attribute.
    public int Robot1Mode
    {
        get { return this.robot1Mode; }
        set { this.SetProperty(ref this.robot1Mode, value); }
    }

    private int robot1Mode;
    internal class MainViewModelDesignInstance : MainPageViewModel
    {
        public MainViewModelDesignInstance()
            : base(null)
        {
        }
    }
}`

this is the code in xaml file:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:App6"
             x:Class="App6.MainPage">

    <Label Text="{Binding Robot1Mode, StringFormat='{0:F2}'}"
           VerticalOptions="Center" 
           HorizontalOptions="Center" />

</ContentPage>
awcullen commented 7 years ago

Do you have 12 namespaces? Maybe this should be ns=2;

nodeId: "ns=12;s=Motion.AxisSet.LocalControl.Axis1.State"

roozbeh63 commented 7 years ago

no that is correct I double checked it.

I can send you a sample of the project that I have? if yes then how can I send it? through email maybe?

awcullen commented 7 years ago

You could put the project in DropBox, but I think you should copy the text from the debug window, and paste it here

roozbeh63 commented 7 years ago

it does't give me any errors, those that I sent were my MainPageViewModel which I inherit from your ViewModelBase. the xaml file was MainPage.xaml and below is my app.cs

` public partial class App : Application { private string discoveryUrl = @"opc.tcp://192.168.0.100:4840"; private ILoggerFactory loggerFactory; private ILogger logger; private UaTcpSessionClient session;

    public App ()
    {
        InitializeComponent();

        MainPage = new App6.MainPage();
    }

    protected override void OnStart ()
    {
        this.loggerFactory = new LoggerFactory();
        this.logger = this.loggerFactory.CreateLogger<App>();

        var appDescription = new ApplicationDescription()
        {
            ApplicationName = "MyHomework",
            ApplicationUri = $"urn:{System.Net.Dns.GetHostName()}:MyHomework",
            ApplicationType = ApplicationType.Client,
        };

        var getEndpointsResponse = UaTcpDiscoveryClient.GetEndpointsAsync(
            new GetEndpointsRequest
            {
                EndpointUrl = this.discoveryUrl,
                ProfileUris = new[] { TransportProfileUris.UaTcpTransport }
            }).Result;
        var selectedEndpoint = getEndpointsResponse.Endpoints.First(e => e.SecurityMode == MessageSecurityMode.None);

        this.session = new UaTcpSessionClient(
            appDescription,
            new DirectoryStore(Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Workstation.ConsoleApp\pki")),
            ed => Task.FromResult<IUserIdentity>(new UserNameIdentity("root", "secret")),
            selectedEndpoint,
            loggerFactory: this.loggerFactory);

        var viewModel = new MainPageViewModel(this.session);
        var view = new MainPage { BindingContext = viewModel };

        this.MainPage = new NavigationPage(view);
    }

    protected override void OnSleep ()
    {
        // Handle when your app sleeps
    }

    protected override void OnResume ()
    {
        // Handle when your app resumes
    }
    private Task<IUserIdentity> ProvideUserIdentity(EndpointDescription endpoint)
    {
        // Due to problem with dns on android emulator, the endpoint url's hostname is rewritten with an ip address.
        endpoint.EndpointUrl = this.discoveryUrl;
        if (endpoint.UserIdentityTokens.Any(p => p.TokenType == UserTokenType.Anonymous))
        {
            return Task.FromResult<IUserIdentity>(new AnonymousIdentity());
        }

        if (endpoint.UserIdentityTokens.Any(p => p.TokenType == UserTokenType.UserName))
        {
            return Task.FromResult<IUserIdentity>(new UserNameIdentity("root", "secret"));
        }

        return Task.FromResult<IUserIdentity>(new AnonymousIdentity());
    }
}`
awcullen commented 7 years ago

Do you have UaExpert (free from Unified-Automation)? With UaExpert you could browse to that node and double-check the nodeid and datatype.

roozbeh63 commented 7 years ago

I will download it asap, but you don't see any problem in the code? right? I mean any obvious problem

awcullen commented 7 years ago

Are you running this Xamarin app on android emulator? Is the server running on the same PC as the emulator or on a different PC?

roozbeh63 commented 7 years ago

I am running Xamarin on android emulator, and the server is a control device from Bosch. it is a Linux embedded device

awcullen commented 7 years ago

Cool. Would you change the code where you create the session to:

        this.session = new UaTcpSessionClient(
            appDescription,
            new DirectoryStore(Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Workstation.ConsoleApp\pki")),
            ProvideUserIdentity,
            selectedEndpoint,
            loggerFactory: this.loggerFactory);
roozbeh63 commented 7 years ago

I just did, but result is the same, still getting 0. and it does not give any errors. it just dons't give me the proper result by some reason.

awcullen commented 7 years ago

Also, I see the problem why you are not getting debug messages. Please change the OnStart:

        this.loggerFactory = new LoggerFactory();
        this.loggerFactory.AddDebug(LogLevel.Trace); // new
        this.logger = this.loggerFactory.CreateLogger<App>();
roozbeh63 commented 7 years ago

I don't have this.loggerFactory.AddDebug in Xamarin, it does not provide me with that function. I have only

AddProvider
CreateLogger
Dispose
Equals
GetHashCode
GetType
ToString
awcullen commented 7 years ago

I forgot to say you will need to add Nuget Package "Microsoft.Extensions.Logging.Debug": "1.1.1",

roozbeh63 commented 7 years ago

that is what i am getting

InspectorDebugSession(40): HandleTargetEvent: ThreadStarted [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Received PublishResponse Handle: 20 Result: 0x00000000 [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Sending PublishRequest Handle: 23 03-24 01:08:24.880 D/Mono (19244): [0xbaedeac0] hill climbing, change max number of threads 2 [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Received PublishResponse Handle: 21 Result: 0x00000000 [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Sending PublishRequest Handle: 24 03-24 01:08:31.900 D/Mono (19244): [0xb9f59ef0] hill climbing, change max number of threads 3 [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Received PublishResponse Handle: 22 Result: 0x00000000 [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Sending PublishRequest Handle: 25 [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Received PublishResponse Handle: 23 Result: 0x00000000 [0:] Workstation.ServiceModel.Ua.Channels.UaTcpSessionChannel: Trace: Sending PublishRequest Handle: 26 InspectorDebugSession(40): Disposed

awcullen commented 7 years ago

That's wonderful. Getting PublishResponses is a good thing. Maybe its time to download UaExpert from UnifiedAutomation to Double check nodeId and datatype.

roozbeh63 commented 7 years ago

oh good, means I am on a right track, I am going to check it asap, thanks for the help, btw you are a great programmer, I learn from reading your codes

roozbeh63 commented 7 years ago

is there anyway to handle errors of connection? or when endpoints can't be retrieved? for example if the IP address of the server is not reachable or server does not respond by any reason.

for example in UnifiedAutomation , for Sessoin we have a ConnectionStatuse method which tells us what is the condition of the connection so I can respond to it accordingly. how I can be doing the same here?

awcullen commented 7 years ago

Today, if the UaTcpSessionClient encounters an error, it just retries the connection after a delay that starts at 1 second and grows to 20 seconds. When the connection is successful, the subscriptions are recreated. In your case, because you initially are using DiscoveryClient to select an endpoint, the OnStart() is having problems if the server is down.

As a workaround I suggest you comment out the discovery:

      /* 
      var getEndpointsResponse = UaTcpDiscoveryClient.GetEndpointsAsync(
            new GetEndpointsRequest
            {
                EndpointUrl = this.discoveryUrl,
                ProfileUris = new[] { TransportProfileUris.UaTcpTransport }
            }).Result;
        var selectedEndpoint = getEndpointsResponse.Endpoints.First(e => e.SecurityMode == MessageSecurityMode.None);
     */

Change SessionClient to use Url

        this.session = new UaTcpSessionClient(
            appDescription,
            new DirectoryStore(Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Workstation.ConsoleApp\pki")),
            ProvideUserIdentity,
           this.discoveryUrl,
            loggerFactory: this.loggerFactory);

Then change ProvideUserIdentity:

private async Task<IUserIdentity> ProvideUserIdentity(EndpointDescription endpoint)
{
    // overide security parameters.
    endpoint.SecurityLevel = 0;
    endpoint.SecurityMode = MessageSecurityMode.None;
    endpoint.SecurityPolicyUri = SecurityPolicyUris.None;

    if (endpoint.UserIdentityTokens.Any(p => p.TokenType == UserTokenType.Anonymous))
    {
        return new AnonymousIdentity();
    }

    if (endpoint.UserIdentityTokens.Any(p => p.TokenType == UserTokenType.UserName))
    {
        return new UserNameIdentity("root", "secret");
    }

    throw new NotImplementedException("ProvideUserIdentity supports only UserName and Anonymous identity, for now.");
}

Let me know if this workaround fixes the OnStart problem. I'll look into ConnectionStatus method. I'm curious to know what events you wish to handle and what action you would take.

Andrew

roozbeh63 commented 7 years ago

I am receiving data from the control now, I traced the node and the problem was with type of variables. also I want to give notifications in my app when:

1- connection is interrupted 2- the app can't find the server IP address 3- endpoint is not available. 4- the Node ID does not exist (which is very essential I think).

with ConnectionStatus I can inform the user that what the app is doing, for example is searching for the connection or connected or disconnected. so I can have a robust solution as an app.

roozbeh63 commented 7 years ago

I have this piece of code to connect the color of a boxview to a variable. I am getting the variable correctly updated, but the color does't change.

`

     #region rTtMotorTempKty1A
        private float _rTtMotorTempKty1A;
        [MonitoredItem(nodeId: "ns=2;s=Application.DataToHmiTab_DrvA.rTtMotorTempKty1")]
        public float rTtMotorTempKty1A
        {
            get { return this._rTtMotorTempKty1A; }
            set { this.SetProperty(ref this._rTtMotorTempKty1A, value); }
        }
        #endregion
    private Color _colorA;
    public Color ColorA
    {
        get
        {
            if (this.rTtMotorTempKty1A > 100)
                this._colorA = Color.Red;
            else
                this._colorA = Color.Blue;
            return this._colorA;
        }
        set { this.SetProperty(ref this._colorA, value); }
    }`
awcullen commented 7 years ago

Try this:

        private float _rTtMotorTempKty1A;
        [MonitoredItem(nodeId: "ns=2;s=Application.DataToHmiTab_DrvA.rTtMotorTempKty1")]
        public float rTtMotorTempKty1A
        {
            get { return this._rTtMotorTempKty1A; }
            set 
            { 
                 this.SetProperty(ref this._rTtMotorTempKty1A, value); 
                 NotifyPropertyChanged(nameof(ColorA));
            }
        }
    public Color ColorA
    {
        get
        {
            if (this.rTtMotorTempKty1A > 100)
                return Color.Red;
            else
                return Color.Blue;
        }
    }
roozbeh63 commented 7 years ago

it got fixed, thanks. do you think it is possible to do something regarding error handing? Connection Status? and when NodeID is not found?

roozbeh63 commented 7 years ago

so is it possible to have error handling system in the package? since I have a deadline for my project and the project is still far from being robust. what do you think I can do? what do you propose?

roozbeh63 commented 7 years ago

can you please tell me even if your answer is no, because I have a deadline for my project and if I am not getting error handling mechanism I have to think about another solution

awcullen commented 7 years ago

I am sorry to hear that you are unhappy with the project. I have noted your suggestions for improvements in the diagnostic area. I will continue working on v2.0! Best of luck.

roozbeh63 commented 7 years ago

I am happy with the project, thanks for the great job. it is just error handling that is being missed. when do you think then second version would be released? or there is no clear timing for it?

awcullen commented 7 years ago

Good things take time. Maybe we could get creative to solve these diagnostic issues. What is the major problem you are facing? Could you share your App.cs and MainPageViewModel.cs?

roozbeh63 commented 7 years ago

the main problem with Connection Status and NodeID when it is not found. now if we can throw one exception for NodeID when it is not found and for Connection Status an event which I can subscribe to.

here you can see my App.cs and part of MainPageViewModel.cs. but I am not getting any errors here, I just want to make my application as robust as possible.

App.cs `public partial class App : Application { private string discoveryUrl = @"opc.tcp://192.168.88.100:4840"; private ILoggerFactory loggerFactory; private ILogger logger; private UaTcpSessionClient session; public App () { InitializeComponent();

        MainPage = new HmiTablet.MainPage();
    }

    protected override void OnStart ()
    {
        this.loggerFactory = new LoggerFactory();
        this.loggerFactory.AddDebug(LogLevel.Trace); // new
        this.logger = this.loggerFactory.CreateLogger<App>();

        var appDescription = new ApplicationDescription()
        {
            ApplicationName = "HmiTablet",
            ApplicationUri = $"urn:{System.Net.Dns.GetHostName()}:HmiTablet",
            ApplicationType = ApplicationType.Client,
        };

        var getEndpointsResponse = UaTcpDiscoveryClient.GetEndpointsAsync(
            new GetEndpointsRequest
            {
                EndpointUrl = this.discoveryUrl,
                ProfileUris = new[] { TransportProfileUris.UaTcpTransport }
            }).Result;
        var selectedEndpoint = getEndpointsResponse.Endpoints.First(e => e.SecurityMode == MessageSecurityMode.None);

        this.session = new UaTcpSessionClient(
 appDescription,
 new DirectoryStore(Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Workstation.ConsoleApp\pki")),
 ProvideUserIdentity,
 selectedEndpoint,
 loggerFactory: this.loggerFactory);

        var viewModel = new MainPageViewModel(this.session);
        var view = new MainPage { BindingContext = viewModel };

        this.MainPage = new NavigationPage(view);
    }

    protected override void OnSleep ()
    {
        // Handle when your app sleeps
    }

    protected override void OnResume ()
    {
        // Handle when your app resumes
    }
    private Task<IUserIdentity> ProvideUserIdentity(EndpointDescription endpoint)
    {
        // Due to problem with dns on android emulator, the endpoint url's hostname is rewritten with an ip address.
        endpoint.EndpointUrl = this.discoveryUrl;
        if (endpoint.UserIdentityTokens.Any(p => p.TokenType == UserTokenType.Anonymous))
        {
            return Task.FromResult<IUserIdentity>(new AnonymousIdentity());
        }

        if (endpoint.UserIdentityTokens.Any(p => p.TokenType == UserTokenType.UserName))
        {
            return Task.FromResult<IUserIdentity>(new UserNameIdentity("root", "secret"));
        }

        return Task.FromResult<IUserIdentity>(new AnonymousIdentity());
    }
}`

MainPageViewModel.cs ` [Subscription(publishingInterval: 250, keepAliveCount: 20)] // Step 2: Add a [Subscription] attribute. public class MainPageViewModel : ViewModelBase {

    #region initialization
    private readonly ILogger<MainPageViewModel> logger;
    private readonly UaTcpSessionClient session;

    public MainPageViewModel(UaTcpSessionClient session)
    {
        this.session = session;
        session?.Subscribe(this);
    }
    #endregion

    #region definition of motor A variables
    #region rTtMotorTempKty1A
    private float _rTtMotorTempKty1A;
    [MonitoredItem(nodeId: "ns=2;s=Application.DataToHmiTab_DrvA.rTtMotorTempKty1")]
    public float rTtMotorTempKty1A
    {
        get { return this._rTtMotorTempKty1A; }
        set { this.SetProperty(ref this._rTtMotorTempKty1A, value);
            //NotifyPropertyChanged(nameof(ColorA));
        }
    }
    #endregion

    private Color _colorA;
    public Color ColorA
    {
        get
        {
            if (this._bSynchronA == false)
                this._colorA = Color.Red;
            else
                this._colorA = Color.Blue;
            return this._colorA;
        }
        set { this.SetProperty(ref this._colorA, value); }
    }

    #region rActualpositionA
    private float _rActualpositionA;
    [MonitoredItem(nodeId: "ns=2;s=Application.DataToHmiTab_DrvA.rActualposition")]
    public float rActualpositionA
    {
        get { return this._rActualpositionA; }
        set { this.SetProperty(ref this._rActualpositionA, value); }
    }
    #endregion

    #region rActualtorqueA
    private float _rActualtorqueA;
    [MonitoredItem(nodeId: "ns=2;s=Application.DataToHmiTab_DrvA.rActualtorque")]
    public float rActualtorqueA
    {
        get { return this._rActualtorqueA; }
        set { this.SetProperty(ref this._rActualtorqueA, value); }
    }
    #endregion`
awcullen commented 7 years ago

I updated UaClient package to 1.5.11.

Please check out MobileDroid sample and notice:

In App.cs, I inserted some code in the ProvideUserIdentity that disables the security settings. (You should fix the security on your server). This approach would allow you to remove your call to GetEndpointsAsync, and has the advantage of automatically retrying every 2-20 seconds until the server is online.

In MainPageViewMode.cs, I added a property IsDisconnected. This is bound to the visibility of a label in MainPage.xaml.

For NodeId errors (Create, Write, etc), I use the system that is provided by INotifyDataErrorInfo in ViewModelBase. In the WPF framework, there is already a UI provided for these Validation errors. I was hoping that Xamarin would include the UI as well, but I have not yet found it. For purposes of the demo I added a property to allow access to the DataErrors of one property. I bound it to a ListView on the MainPage. I got mixed results.

abrasat commented 7 years ago

Can the opc-ua client handle also user validation errors from OPC-UA servers (wrong credentials) ? What about the handling of errors regarding the permitted actions for validated users (for instance the actions permitted for validated user allow the reading of opc-ua items, but not the writing of values for opc-ua items) ?

awcullen commented 7 years ago

When the application starts, UaTcpSessionClient will begin opening a channel to the server. First it uses DiscoveryClient to get the list of endpoints from the provided url. Second, it chooses the most secure endpoint and calls the 'ProvideUserIdentiy' callback. This call is asynchronous, so you are free to open a dialog window and have your users enter a username and password. When the callback returns with a IUserIdentity, the UaTcpSessionClient finishes opening the channel. If the userName or password is wrong, then UaTcpSessionClient will retry from the start. Each retry is delayed from 2-20 seconds.

awcullen commented 7 years ago

If your user is not permitted to Write certain nodes, then the server will return a bad status code. In the case of the MainPageViewModel (above), the write error is stored in the errors collection in ViewModelBase. The WPF framework has built-in styles that highlight the UI with a red border and display the error message. In other frameworks, you have to listen for ErrorsChanged event and call GetErrors()

awcullen commented 7 years ago

V2.0.0-RC2 on NuGet has the following: View-models can inherit from SubscriptionBase, which implements INotifyPropertyChanged. SubscriptionBase automatically connects to specified server endpointUrl and creates the opc-ua subscription and monitored items. It receives the publish responses and places the dataValue or event in the specified view-model properties. It provides a property InnerChannel that allows you to call any opc-ua method such as Read, Write, CallMethod, etc. It provides a property State that indicates the CommunicationState of the connection. If errors occur with creating, publishing or writing Monitored Items, the error message is provided by the INotifyDataErrorInfo interface. If errors occur with the InnerChannel, then the State property is changed to Faulted, and the channel is recreated automatically.

I hope this is helpful. Closing this issue for now.

roozbeh63 commented 7 years ago

did you remove IsDisconnected from the repository? I can't find it anymore, and I tried to find it in commit history, but no luck, is it possible to commit the same version again? thanks.

awcullen commented 7 years ago

IsDisconnected has been replaced by:

        /// <summary>
        /// Gets the <see cref="CommunicationState"/>.
        /// </summary>
        public CommunicationState State { get; }

Where CommunicationState is:

    public enum CommunicationState
    {
        Created,
        Opening,
        Opened,
        Closing,
        Closed,
        Faulted
    }
abrasat commented 7 years ago

Is there also some event available, triggered when communication state changes (something like CommunicationStateChangedEvent) ?

Regarding the INotifyDataErrorInfo, could you please provide a sample about how to use it in a non-UI application ? For instance could you add it to the

[TestMethod]
public async Task TestSubscription()
{...}

from UnitTest1.cs ? Would be for instance useful to see what errors are generated in opc-ua client over INotifyDataErrorInfo when the opc-ua-server uri is not reachable, or when the node id is unknown on server.