1

I'm trying to write C# application where ADS client will automatically reconnect/renew reading/writing values and subscriptions when application lost connection with PLC or new version of PLC program will be downloaded.

I'm using TwinCAT.Ads.Reactive v4.4.0 library from NuGet.

My program flow is:

Connect to ADS server (connection successful)=>

  1. Create reactive notification with ValueSymbolExtensions.WhenValueChanged
  2. Create reactive cyclic write value ValueSymbolExtensions.WriteValues

    • 3[a] Create reactive cyclic poll values AnyTypeExtensions.PollValues.T
    • 3[b] I have also tried ValueSymbolExtensions.PollValues which is not documented yet on Beckhoff website

So far I have find out that 1. and 2. works even if I unplug ethernet cable or downlaod new program to PLC -> the WriteValues() and WhenValueChanged() renew itself internally

CODE:

// WhenValueChanged()
TreeViewSymbols = SymbolLoaderFactory.Create(_client, SymbolLoaderSettings.Default).Symbols;  // Load symbol tree from plc

IValueSymbol boolVal = (IValueSymbol)TreeViewSymbols["SomeBoolValue"];
boolVal.WhenValueChanged().Subscribe(Observer.Create<object>(val => ArchiveData((bool)val)));

// WriteValues()
IValueSymbol toggleBit = (IValueSymbol)TreeViewSymbols["toggle_bit"];
            toggleBit.WriteValues(
                  Observable.Interval(TimeSpan.FromSeconds(1.0)).Select(x => x % 2 == 0 ? false : (object)true),
                  e => Debug.WriteLine($"Error writing toggle bit")
            );

I read in PollValues() every second custom struct of Data. During normal run this works fine, but after I change PLC program and download changes to PLC or connection is lost (unpluged ethernet cable) this subscription fails internally and do not recover like the ones above.

IValueSymbol state = (IValueSymbol)TreeViewSymbols[Cfg.ModuleStateTag];
state.PollValues(TimeSpan.FromSeconds(1.0))
      .Subscribe(Observer.Create<object>(
       val => // val comes as byte[] array
      {
         var a = new ModuleStateData((byte[])val);
         Debug.WriteLine($"Status values Machine Mode:{a.MachineMode}");
      },
      e => Debug.WriteLine($"Error reading status"),
      () => Debug.WriteLine($"OnComplete???? reading status"))
      ).AddDisposableTo(_disposables);

_client.PollValues<ModuleStateData>(
                Cfg.ModuleStateTag,
                TimeSpan.FromSeconds(1.0)
                ).Subscribe(Observer.Create<object>(
                val =>
                {
                    Debug.WriteLine($"Status values Machine Mode:{val.MachineMode});
                },
                e => Debug.WriteLine($"Error reading status - {e.Message}"),
                () => Debug.WriteLine($"OnComplete???? reading status"))
            ).AddDisposableTo(_disposables);

ConnectionStateChanged

Also connection state changed event is fired only if I call Connect()/Disconnect() on ads client, and not on connection problems. Any idea how can I find out there is a connection problem?

magino
  • 109
  • 9

2 Answers2

1

I have found out solution on half of my problem where observable works even on connection error(unpluged ethernet cable), but after download new PLC program(ads symbol version is changed) it do not recreate variable with new version, so it just throws error.

Solution is in using another overload of PollValues where I specify Func<Exception, T> errorHandler. This handler works like backup value, in case of error.

_client.PollValues<ModuleStateData>(
                Cfg.ModuleStateTag,
                TimeSpan.FromSeconds(1.0),
                e =>
                {
                    Debug.WriteLine($"Error reading status {Cfg.Name} - {e.Message}");
                    return new ModuleStateData()
                    {
                        // Set data in case of error
                    };     
                }
            );
magino
  • 109
  • 9
  • why using polling.. have you consider doing event approach – T-Rex Mar 06 '20 at 04:49
  • @Laksh I want to prevent any loss of data (lost message or something), these are state data and if I miss one notification, that could lead to problem. But i would also like to hear your opition on this. And one more question - Can I set subscription on custom data type? If yes, will it notify me if only one field will change from that data type? – magino Mar 09 '20 at 22:01
1

I have report this issue to creators of nuget package.

Answer 6.3.2020:

Thanks for reporting this issue, this is an important aspect. I had a short look into your case. Unfortunately, an automatic resurrection of observable is not supported actually. The reason is that the PollValues internally uses Symbol handles, which will be invalidated when a now PLC Program is updated. So actually, your only solution is to register for the SymbolVersionChanged event on the TcAdsClient/AdsConnection (this is sent after a download/restart) and recreate the Observable.

https://infosys.beckhoff.de/content/1031/tc3_adsnetref/7313543307.html?id=2192955395989567903

If it is appropriate for you to wait for the next version of the AdsClient / Ads.Reactive package – it should be possible to handle this situation internally int the PollValues code (in the way you expect). I have it now on my TODO List.


Before I got this answer I have edited their implementation and created my own extension method.

TwinCAT.Ads.Reactive v4.4.0 implementation

    public static IObservable<T> PollValues<T>(
      this IAdsConnection connection,
      string instancePath,
      int[] args,
      IObservable<Unit> trigger,
      Func<Exception, T> errorHandler)
    {
      DisposableHandleBag bag = new DisposableHandleBag(connection, (IList<string>) new string[1]
      {
        instancePath
      });
      Func<Unit, T> selector = (Func<Unit, T>) (o =>
      {
        try
        {
          return (T) connection.ReadAny(61445U, bag.GetHandle(instancePath), typeof (T), args);
        }
        catch (Exception ex)
        {
          if (errorHandler != null)
            return errorHandler(ex);
          throw;
        }
      });
      Action finallyAction = (Action) (() =>
      {
        bag.Dispose();
        bag = (DisposableHandleBag) null;
      });
      return trigger.Select<Unit, T>(selector).Finally<T>(finallyAction);
    }

My edit - Where I create handle for every read and delete handle after read

    public static IObservable<T> MyPollValues<T>(
            this IAdsConnection connection,
            string instancePath,
            int[] args,
            IObservable<Unit> trigger,
            Func<Exception, T> errorHandler)
        {
            Func<Unit, T> selector = (Func<Unit, T>)(o =>
            {
                try
                {
                    var handle = connection.CreateVariableHandle(instancePath);
                    var data = (T)connection.ReadAny(handle, typeof(T), args);
                    connection.DeleteVariableHandle(handle);
                    return data;
                }
                catch (Exception ex)
                {
                    if (errorHandler != null)
                        return errorHandler(ex);

                    throw;
                }
            });
            return trigger.Select<Unit, T>(selector);
        }
magino
  • 109
  • 9