# Schnelle Datenübertragung mit DotNetSiemensPLCToolBoxLibrary (LibNoDave)



## Hans54216 (21 September 2015)

Ich hab mit der DotNetSiemensPLCToolBoxLibrary einen Datenaustausch zwischen Siemens 840d sl (CPU 317F-3 PN/DP) und einer Beckhoffsteuerung (TwinCAT 2) aufgebaut.

Es werden 4 Arrays(2 x 30DWORD, 2x 20DWORD) + Lifebit von der Beckhoffsteuerung in DB800 und ein Array(30 DWORD) vom DB800 zur Beckhoffsteuerung übertragen.

Für ein Array benötige ich ca. 15 ms, in Summe ca. 70 - 100 ms.

Ich möchte aber gern in Summe auf 15 - 25 ms (PLC Zykluszeit) kommen.

Wenn ich im selben Programm mehrere Verbindungen zur Steuerung erstelle, verändert sich die Zeit nicht. Starte ich jedoch mehrere Programme, welche jeweils nur ein Array lesen, liegt die Zeit bei ca. 30 ms.


Wie schaff ich es in einem Programm auf diese Übertragungszeit zu kommen?

Danke!


----------



## Jochen Kühner (21 September 2015)

Läuft denn dein Code auch parallel wenn du es im selben Program machst? Poste doch mal etwas davon!


----------



## Jochen Kühner (21 September 2015)

Noch zur Info: Du kannst aber nicht sagen zu welchem Zeitpunkt deines SPS Zyklus die Daten gelesen werden! Falls du das brauchst musst du über die Vartab-Funktionen lesen!!


----------



## Hans54216 (22 September 2015)

Wie verhält sich die Vartab-Funktion bezüglich der Übertragungsgeschwindigkeit?

Wenn ich den "ReadSpeedtest" ausführe kommt zwar nur eine leicht erhöhte Zeit für die Übertragung raus, jedoch friert das Tool mehrere Sekunden ein. Laut Hinweis wird ja nur die Übertragungszeit und nicht die Anfrage gemessen.
Kann ich dann davon ausgehen, dass die Funktion in Summe mehrere Sekunden benötigt?


----------



## Hans54216 (22 September 2015)

Jochen Kühner schrieb:


> Läuft denn dein Code auch parallel wenn du es im selben Program machst? Poste doch mal etwas davon!







```
public partial class FormTestNeu1_2 : Form
{
    private static Dictionary<string, CTestNeu1_2> DataHandles = new Dictionary<string, CTestNeu1_2>();
    
    public FormTestNeu1_2()
    {
        InitializeComponent();
        FileTransfer();
    }
            
    private void FileTransfer()
    {
        DataHandles.Add("Wert", new CTestNeu1_2(
                    Properties.Settings.Default.TwinCAT_NetID,
                    Properties.Settings.Default.TwinCAT_Port,
                    Properties.Settings.Default.TwinCAT_Wert_Path,
                    Properties.Settings.Default.S7Var_Wert,
                    "Wert",
                    true));
    
        DataHandles.Add("Wert2", new CTestNeu1_2(
                    Properties.Settings.Default.TwinCAT_NetID,
                    Properties.Settings.Default.TwinCAT_Port,
                    Properties.Settings.Default.TwinCAT_Wert2_Path,
                    Properties.Settings.Default.S7Var_Wert2,
                    "Wert2",
                    true));
        ...
        DataHandles["Wert"].StartDataTransfer();
        DataHandles["Wert2"].StartDataTransfer();
        DataHandles["Status"].StartDataTransfer();
        DataHandles["Status2"].StartDataTransfer();
        DataHandles["Output"].StartDataTransfer();
    }
    ...
    
public class CTestNeu1_2
{
...
    public CTestNeu1_2(string _AmsNetID, int _Port, string _SymbolName, string _S7FormatAdress, string _SiemensConnectionName, bool _read)
    {
        WriteToSiemens = _read;
        Port = _Port;
        AmsNetID = _AmsNetID;
        SymbolName = _SymbolName;
        S7FormatAdress = _S7FormatAdress;
        S7Connect.ConnectionName = _SiemensConnectionName;
    }
    
    private bool _DataTransferStarted = false;
    public void StartDataTransfer()
    {
        if (_DataTransferStarted)
            return;




        Thread ThreadHandle = new Thread(new ThreadStart(DataHandle));
        ThreadHandle.IsBackground = true;
        ThreadHandle.Start();
        _DataTransferStarted = true;
    }
    
    private void DataHandle()
    {
       ...
       while (!AbortRequestedStep)
       {
          if (WriteToSiemens)
          {
             //Beckhoff PLC Variablen lesen
             firstTime2 = Environment.TickCount;
             oBuffer = tcClientAdmin.ReadAny(_SymbolInfo.indexGroup, _SymbolInfo.indexOffset, _SymbolInfo.type, _SymbolInfo.args);
             timeBeckhoff = Environment.TickCount - firstTime2;
             
             
             //Siemens schreiben in DB
             firstTime2 = Environment.TickCount;
             if (oBuffer != null)
                 VerbindungsinformationenSiemens = S7Connect.writeDB(S7FormatAdress, (int[])oBuffer, _SymbolInfo.args[0]);
             else
                 VerbindungsinformationenSiemens = string.Empty;
             timeSiemens = Environment.TickCount - firstTime2;
          }
          ...
       }
    }
```


----------



## Jochen Kühner (22 September 2015)

Wie misst du denn die Zeit die alle request brauchen?

Wie wär's denn 4 Tasks zu starten, und dann mit Task.WhenAll zu warten...


----------



## Hans54216 (23 September 2015)

Ich hab jetzt mal eine Versuchsreihe erstellt:


```
public Form1()
{
    connect("Wert");
    connect("Wert2");
    connect("Status");
    connect("Status2");
    lWert = getPLCTags("P#DB801.DBX400.0 DINT 30", new int[] { 1 });
    lWert2 = getPLCTags("P#DB801.DBX520.0 DINT 20", new int[] { 1 });
    lStatus = getPLCTags("P#DB801.DBX800.0 DWORD 30", new int[] { 1 });
    lStatus2 = getPLCTags("P#DB801.DBX920.0 DWORD 20", new int[] { 1 });

    int firstTime = Environment.TickCount;
    Parallel.Invoke(Task1, Task2, Task3, Task4);
    string time = (Environment.TickCount - firstTime).ToString();
    Console.WriteLine("Time Elapsed TickCount: " + time);
}


static void Task1()
{
     myConn["Wert"].WriteValues(lWert, true);
}
```


Eine Verbindung ohne Optimierung | mit Optimierung | vier Verbindungen ohne Optimierung | mit Optimierung








Jetzt stellt sich mir nur die Frage, wie viel in ein _PDU rein geht, um die richtige Anzahl an Verbindungen zur Steuerung aufzubauen._


----------



## Jochen Kühner (24 September 2015)

Wenn nur aus einem Bereich gelesen werden soll: 22Bytes, siehe: http://www.sps-forum.de/hochsprachen-opc/25407-pdu-groesse-und-tcp-ip-frame.html#post182809


----------



## Jochen Kühner (24 September 2015)

Ich hab hier gepostet: http://www.sps-forum.de/hochsprache...ox-library-kuehner-post596838.html#post596838 wie Ich das mit dem Parallelen lesen machen würde...


----------



## Hans54216 (24 September 2015)

Jochen Kühner schrieb:


> Wenn nur aus einem Bereich gelesen werden soll: 22Bytes, siehe: http://www.sps-forum.de/hochsprachen-opc/25407-pdu-groesse-und-tcp-ip-frame.html#post182809



"240 und 480 Bytes PDU-Size. Davon geht der Overhead weg und es bleiben beim Lesen aus einem Bereich 222 bzw. 462 Bytes übrig"


Vielen Dank für deine Hilfe!!


----------



## Burkhard (24 September 2015)

Hallo Hansi, ich beschaeftige mich auch mit diesem Thema und habe daher nochmal einen Extra-thread erstellt, mit einem kompletten vb.net Projekt und ein paar Dokumentations-Bildern. Ich hoffe du kannst damit auch was anfangen.


----------



## Burkhard (26 September 2015)

Ich will eine automatische Funktion schreiben, wo du als Eingangliste die Konfigurationsliste aller Tags gibst und als Ausgaenge bekomsmt du EINE Liste von PLCTag-Listen und eine Liste von Connections, jeweils so aufgesplittet, dass jeweils eine Tag- Liste in eine PDU passt.

Dazu muss man die MaxPDULen erstmal ermitteln. Libnodave hatte dafuer eine Funktion. Soweit ich sehe gibt es diese in der Toolbox-Library nicht mehr. Dann muss ich das ueber die JFK-Libnodave Library direkt machen. Waere ja auch kein Problem.

Da mache ich mich jetzt mal dran.


----------



## Burkhard (26 September 2015)

Jochen Kühner schrieb:


> Wenn nur aus einem Bereich gelesen werden soll: 22Bytes, siehe: http://www.sps-forum.de/hochsprachen-opc/25407-pdu-groesse-und-tcp-ip-frame.html#post182809



Du meinst doch 222 Bytes und nicht etwa 22 Bytes??


----------



## Jochen Kühner (26 September 2015)

ja, natürlich


----------



## Burkhard (26 September 2015)

```
Private Function CreateTagListsFromDeviceConfig(tblConfig As DataTable, ByRef stLine As String) As List(Of List(Of PLCTag))
        Dim listPlcTagList As New List(Of List(Of PLCTag)) ' List of PLC tag lists
        Dim lstPlcTag As List(Of PLCTag) ' List of PLC tags (one for each PDU)
        lstPlcTag = New List(Of PLCTag) ' List of PLC tags (one for each PDU)
        Dim iSize1 As Integer = 0
        For Each row As DataRow In tblConfig.Rows
            Dim rowSuccessor As DataRow
            If tblConfig.Rows.IndexOf(row) < tblConfig.Rows.Count - 1 Then rowSuccessor = tblConfig.Rows(tblConfig.Rows.IndexOf(row) + 1)
            Dim myPlcTag As New Communication.PLCTag(row.Item("Address").ToString)
            Dim stType As String = row.Item("Type").ToString.Trim
            If stType.Contains("REAL") Then
                myPlcTag.TagDataType = TagDataType.Float
                myPlcTag.ValueName = row("Name").ToString
                lstPlcTag.Add(myPlcTag)
                iSize1 += GetSize(stType)
            ElseIf stType.Contains("INT") Then
                myPlcTag.TagDataType = TagDataType.Int
                myPlcTag.ValueName = row("Name").ToString
                lstPlcTag.Add(myPlcTag)
                iSize1 += GetSize(stType)
            ElseIf stType.Contains("STRING") Then
                myPlcTag.TagDataType = TagDataType.String
                myPlcTag.ArraySize = GetSize(stType)
                myPlcTag.ValueName = row("Name").ToString
                lstPlcTag.Add(myPlcTag)
                iSize1 += GetSize(stType)
            ElseIf stType.Contains("DWORD") Then
                myPlcTag.TagDataType = TagDataType.Dword
                myPlcTag.ValueName = row("Name").ToString
                lstPlcTag.Add(myPlcTag)
                iSize1 += GetSize(stType)
            End If
            If tblConfig.Rows.IndexOf(row) < tblConfig.Rows.Count - 1 Then
                Dim iSize2 = CInt(GetSize(rowSuccessor.Item("Type")).ToString.Trim)
                If iSize1 + iSize2 > 222 Then
                    listPlcTagList.Add(lstPlcTag)
                    iSize1 = 0
                    lstPlcTag = New List(Of PLCTag) ' List of PLC tags (one for each PDU)
                End If
            Else
                listPlcTagList.Add(lstPlcTag)
            End If

            stLine = stLine & "," & row("Name")
        Next
        CreateTagListsFromDeviceConfig = listPlcTagList
    End Function
```

Hier ist meine Funktion, die die Ausgangs-Variablen-Liste in mehrere Tag-Listen aufsplittet. Das Ergebnis ist eine List of List. Ganz interessant, nicht wahr?

Jede dieser Tag-Lists wird dann einer Verbindung zugeordnet. Die Funktion, die die Verbindungs-Liste erzeugt kommt hier:


```
Private Function CreateConnectionListFromTagLists(stIPAddress As String, listPLCTagList As List(Of List(Of PLCTag))) As List(Of PLCConnection)
        Dim listConnection As New List(Of PLCConnection)
        Dim index As Integer = 0
        For Each listPLCTag As List(Of PLCTag) In listPLCTagList
            Dim myConn As New PLCConnection("Connection" & CStr(iDeviceIndex) & "_" & listPLCTagList.IndexOf(listPLCTag))
            myConn.Configuration.CpuIP = stIPAddress
            myConn.Configuration.CpuRack = 0
            myConn.Configuration.CpuSlot = 2
            myConn.Configuration.Port = 102
            myConn.Configuration.TimeoutIPConnect = 5000000
            myConn.Configuration.ConnectionType = LibNodaveConnectionTypes.ISO_over_TCP
            listConnection.Add(myConn)
            index += 1
        Next
        CreateConnectionListFromTagLists = listConnection
    End Function
```

Dann werden im naechsten Schritt fuer jede Connection ein eigener Thread gestartet. Immer im Hinterkopf behalten, dass neben dem Main-Thread bereits fuer jede SPS ein eigener Thread gestartet wurde. Es handelt sich also bei diesen Connection-Threads um Sub-Threads des SPS-Threads. Sub-Sub-Threads wie in einem Baum. Ganz interessant nicht wahr?

Jetzt muss ich nur noch aus den Ergebnis-Tag-List eine Gesasmt-Liste zusammenbasteln. 

Ein kleines Problem habe ich noch. Es geht darum, dass ich EIGENTLICH, die Daten aus EINER SPS immer in EINER Datei speichern wollte, mit EINEM Zeitstempel verstehen, welcher im vb.net Programm gebildet wird.

Da ich haber unabhaengige Threads pro Connection habe, fallen Daten mit unterschiedlicen Zeitstempeln an, die ich nun schwierig in einer Datei unterbringen kann. Werde daher wohl im ersten Schritt pro Connection eine eigene Datei schreinen. Da es pro SPS bis zu vier Connections (bisher, koennen aber auch beliebig mehr sein), kommen dann bei 14 SPSen ziemlich viele Dateien dabei raus. Das hatte ich mir eigentlich anders vorgestellt. 

Wie ich die Threads allerdings syncronisiere weiss ich noch nicht und wenn ueberhaupt, dann muessen sie ja aufeinander warten und dann geht der Geschwindigkeitsvorteil wieder floeten.

Wenn ich die App wieder auf Stand habe, stelle ich das komplette Projekt hier rein, damit Interessenten es nutzen koennen.


----------



## Jochen Kühner (27 September 2015)

So eine funktion wie du da geschrieben hast hab Ich ja auch drinn, wenn man die read funktion mit der optimierung nutzt! Diese ist noch etwas cleverer und fast dann variablen aus den selben bereichen zusammen, und berücksichtigt die request header größe, ob ein zusammenfassen günstiger ist! Jedoch liest diese dann alles über eine connection! Ich könnte mir überlegen diese funktion zu exportieren, oder das lesen über mehrere verbindungen direkt einzubauen.

wie du threads synchronisieren würdest hab ich hier gepostet : http://www.sps-forum.de/hochsprache...ox-library-kuehner-post596838.html#post596838 also ich nutze keine threads sondern tasks! und falls dies nicht schneller ist, bringt die ganze parallele verarbeitung nichts, da dann die sps das nicht wirklich parallel macht!!


----------



## Burkhard (29 September 2015)

Doch doch, die parallele Verarbeitung bringt sehr viel. Erstens jede SPS in einem eigenen Thread. Und dann jede Verbindung zur SPS nochmal in einem eigenen Thread, wobei man pro Verbindung nur eine PDU liest, bis die Anzahl maximal möglicher Verbindungen aufgebraucht ist... Das zu managen und zu überwachen ist ein bisschen extra Programmieraufwand, aber es lohnt sich, wenn man auf Geschwindigkeit optimieren muss.


----------



## Jochen Kühner (29 September 2015)

ja, nur nicht das dunur die zeit in dem thread misst, sondern wie lange die lesethreads brauchen wenn man sie synchronisiert! d.h. das wenn ich alle requests an die cpu schicke dauert es bis ich alle antworten habe x milisekunden!


----------

