# Wie VBA Scripte mit der S7 verbinden



## tommylik (22 Dezember 2020)

Guten Morgen,

Habe derzeit ein S7-Projekt auf einer 416F-3 laufen.
Programmerstellung wird mit der 5.6 gemacht.
Die Visualisierung wird mit Zenon von Copa-Data dargestellt.
Es gibt 12 Triggersignale im S7 Programm die jeweils ein VBA Script in Zenon auslöst.
Die Scripte kopieren Daten zu verschiedenen Zeitpunkte auf den Server.
Eigentlich eine gute Idee funktioniert auch ist aber nicht 100% tauglich für die Produktion wegen der möglichen mehrfachen aufrufen pro Zyklus.
Es wurden auch schon Verriegelungen bedacht, das die Scripte nacheinander abgearbeitet werden.
Damit die Visu nicht sporadisch blockiert wird. 
Das Problem ist die RT von Zenon, die unterstützt keine Multiinstanzen das mehrere Scripte parallel laufen können.

Meine Frage an Euch mit welcher anderen externen Software (WinCC ist keine Option) 
die ich mit der S7 verbinden kann, VBA unterstützt und auch unterstützt das mehrere Scripte gleichzeitig ausgeführt werden 
könnte ich das umsetzen? 

Für Eure Ideen vielen Dank im voraus.

Mfg Tommylik


----------



## oliver.tonn (22 Dezember 2020)

Muss denn definitiv zum Zeitpunkt des Triggersignals geschrieben werden? Falls nein und die Trigger nicht zu oft kommen könntest Du ja die Trigger in ein Array schreiben und in ZenOn dies Element für Element abarbeiten lassen und immer wenn ein Element TRUE ist wird das entsprechende Script abgearbeitet und dann geht es weiter, allerdings müsstest Du Dir dann noch ein Handshake Verfahren ausdenken, damit ein Script nicht mehrfach ausgeführt wird weil der Trigger vielleicht noch anliegt.


----------



## tommylik (22 Dezember 2020)

Hallo Oliver,

Vielen Dank für deine Antwort.

Wäre besser damit die RT bis zum nächsten anderem Triggersignal wieder frei ist.
Es kann ja auch vorkommen das mehrere Triggersignale gleichzeitig anstehen.
Das habe ich schon so verriegelt das ein Signal nach dem anderen abgearbeitet wird.
Es ist auch schon bedacht das wenn ein Script ausgeführt wird das das Triggersignal am Ende des Scriptes auf 0 gesetzt wird.
Somit kann der Trigger nicht mehr anliegen.
Mein Problem ist eigentlich mehr das die RT blockiert werden könnte weil halt die Scripte erst fertig sein müssen.
Einige Scripte brauchen länger weil die zu kopieren Daten höher sind.

Wenn die Scripte gleichzeitig laufen könnten wäre auch die RT nicht belegt. Wie Multi-Instanz.


Vielen Dank nochmal für deine Hilfe

Mfg Tommylik


----------



## Larry Laffer (22 Dezember 2020)

@Tommy:
Wenn hier wirklich VBA deine Basis ist dann wird es schwierig. Vielleicht könnte man etwas raten wenn wir den Code deiner Routine sehen können. Aber generell solltest du da eher Richtung .Net gehen (also z.B. VB.Net). Hier könntest du schon erreichen, dass deine Visu selbst responsibel bleibt (und das heißt nicht zwangsläufig, dass du Tasks oder Backgroundworker programmieren müßtest ...

Gruß
Larry


----------



## tommylik (22 Dezember 2020)

Hallo Larry,

Vielen Dank für deine Antwort.

Also VBA ist meine Basis aber Zenon kann auch über Projekt Add-Ins VB.net oder C#.
Aber laut dem Copa-Data Support würde mir VB.net oder C# auch nicht helfen da die RT das Problem ist.
Egal wie ich es umsetzte die RT kann nur ein Script nach dem anderen abarbeiten.

Es sei denn du kennst dich mit Zenon aus und kennst ein paar Tricks.

Hier die VBA Version.





Hier der Code vom Zenon Projekt.
Der ist sehr lang ich habe fast alles raus gelöscht was nicht mit dem Thema zu tun hat.
Falls etwas unlogisch sein sollte dann habe ich etwas zu viel gelöscht.



```
Option Explicit


' Definition der Container mit Onlinevariablen für
' jeden Zugriff auf Runtimevariablen aus dem VBA-Skript heraus.


Public WithEvents Container As OnlineVariable
Public PDE_Container As OnlineVariable


Public Sub Profinet_Init()


'##### PROFINET-DIAGNOSE INITIALISIEREN #####


Dim i As Integer
Dim Profinet As Variable


For i = 1 To 256
    PN_Container.Add "Profinet_Station[" & i & "]"
Next i
PN_Container.Add "Profinet_Device_aktiv"
PN_Container.Define


'interne Variablen für Profinet-Devicenummer initialisieren
For i = 1 To 256
    
    Set Profinet = thisProject.Variables.Item("Profinet_Station[" & i & "]")
    Profinet.Value = i


Next i


thisProject.RtFunctions.Item("fct_Profinet_Systemnummer_setzen").start


'Vorbereitung zum Anklicken einer PROFINET-Station
PN_Container.Undefine
PN_Container.Add "Profinet_Device_aktiv"
PN_Container.Define
    
End Sub




Public Sub Profinet_Detailfenster_verschieben()


'##### PROFINET-DETAILFENSTER VERSCHIEBEN #####


    Dim meinBild As DynPicture
                 
    Set meinBild = thisProject.DynPictures.Item("Profinet_Details")
        
 
    ' Positionskorrektur in x-Richtung
    If xkoord > (1280 - meinBild.Width) Then ' Falls der rechte Rand erreicht ist, das Bild an die linke Seite anheften
    
        xkoord = xkoord - meinBild.Width - breite ' x-Koordinate um Breite des Detailfensters und des Stationssymbols verschieben
        
    End If
    
    ' Positionskorrektur in y-Richtung (124 steht für die Höhe der Kopfzeile)
    If (ykoord + 124) > (1024 - meinBild.Height) Then ' Falls der untere Rand erreicht ist, das Bild entsprechne nach oben schieben
    
        ykoord = ykoord - ((thisProject.ykoord + 124) - (1024 - meinBild.Height)) ' Differenz abziehen, um Bild höher schieben
        
    End If
    
    ' Bild an neue Position verschieben
    ' wird normalerweise direkt neben rechtem Eck des Symbols geöffnet
    ' (Der Wert 124 ist die Höhe der Kopfzeile)
    
    meinBild.Move xkoord, ykoord + 124, meinBild.Width, meinBild.Height
   
End Sub


Private Sub Project_Active()


'##### STARTRUTINE DER RUNTIME #####
    
  
    Set PN_Container = thisProject.OnlineVariables.CreateOnlineVariables("profinet")
    Set STG_Container = thisProject.OnlineVariables.CreateOnlineVariables("stellgeraete")
    Set Ident_Container = thisProject.OnlineVariables.CreateOnlineVariables("ident")
    Set Typ_Container = thisProject.OnlineVariables.CreateOnlineVariables("typverwaltung")
    Set Schrauber_Container = thisProject.OnlineVariables.CreateOnlineVariables("schrauberkrempl")
    
   'Container mit den Variablen für die Rueckverfolgung
    Set PDE_Container = thisProject.OnlineVariables.CreateOnlineVariables("PDE")
 
    PDE_Container.Add "PDE_PraegeCode"
    PDE_Container.Add "PDE_OrdnerName"
    PDE_Container.Add "PDE_OrdnerErstellen"

    PDE_Container.Add "PDE_Bolzen10R01"
    PDE_Container.Add "PDE_MS15R01"
    PDE_Container.Add "PDE_Kamera15R01"
    PDE_Container.Add "PDE_Kelchung15R01"
    PDE_Container.Add "PDE_Bolzen15R02"
    PDE_Container.Add "PDE_Intec15R02"
    PDE_Container.Add "PDE_HWH30R01"
    PDE_Container.Add "PDE_HWH30R02"
    PDE_Container.Add "PDE_HWH40R01"
    PDE_Container.Add "PDE_HWH50R01"
    PDE_Container.Add "PDE_HWH60R01"
    PDE_Container.Add "PDE_HWH60R02"
   
    PDE_Container.Define
        
End Sub


Private Sub Project_Inactive()
    
    '##### ENDERUTINE DER RUNTIME #####
    
    On Error Resume Next
    Container.Undefine ' Online Container wieder löschen
    Debug.Print "Der Container ist undefined"
       
    PDE_Container.Undefine
    thisProject.OnlineVariables.DeleteOnlineVariables ("PDE")
   
End Sub
  


'Dateien vom Bolzen kopieren 10R01
Public Sub Bolzen_10R01()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


'Fehlerroutine: Datei nicht vorhanden
If oFSO.FileExists("\\TOX10R01\AppData\" & strDatei & "TOX10R01.txt") = False Then
   'File doesn't exist
thisProject.Variables.Item("PDE_10R01BolNoFile").Value = 1
Else
   'File exist
oFSO.CopyFile "\\TOX10R01\AppData\" & strDatei & "TOX10R01.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_Bolzen10R01").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub




'Dateien von der Kelchung kopieren 15R01
Public Sub Kelchung_15R01()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\TOX15R01\AppData\" & strDatei & "TOX15R01.txt") = False Then
   'File doesn't exist
thisProject.Variables.Item("PDE_15R01KelNoFile").Value = 1
Else
   'File exist
oFSO.CopyFile "\\TOX15R01\AppData\" & strDatei & "TOX15R01.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_Kelchung15R01").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Bolzen kopieren 15R02
Public Sub Bolzen_15R02()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


Set oFSO = CreateObject("Scripting.FileSystemObject")


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


If oFSO.FileExists("\\TOX15R02\AppData\" & strDatei & "TOX15R02.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_15R02BolNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\TOX15R02\AppData\" & strDatei & "TOX15R02.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_Bolzen15R02").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Kleben kopieren 15R02
Public Sub Intec_15R02()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\INT15R02\AppData\" & strDatei & "INT15R02.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_15R02IntNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\INT15R02\AppData\" & strDatei & "INT15R02.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_Intec15R02").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schweißen kopieren 30R01
Public Sub HWH_30R01()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH30R01.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_30R01HwHNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH30R01.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_HWH30R01").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schweißen kopieren 30R02
Public Sub HWH_30R02()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH30R02.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_30R02HwHNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH30R02.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_HWH30R02").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schweißen kopieren 40R01
Public Sub HWH_40R01()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH40R01.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_40R01HwHNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH40R01.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_HWH40R01").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schweißen kopieren 40R02
Public Sub HWH_40R02()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH40R02.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_40R02HwHNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH40R02.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_HWH40R02").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schweißen kopieren 50R01
Public Sub HWH_50R01()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH50R01.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_50R01HwHNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH50R01.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_HWH50R01").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schrauber kopieren 60R01
Public Sub Schrauber_60R01()


Dim strNr As Variant
Dim strOrdner As String
Dim oFSO As Object


strNr = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNr), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


'Fehlerroutine: Datei nicht vorhanden
If oFSO.FileExists("D:\S7_Export\Schrauber\????\*.*", "D:\S7_Export\AppData\" & strOrdner & "\") = False Then
   'File doesn't exist
thisProject.Variables.Item("PDE_60R01RexNoFile").Value = 1
Else
   'File exist
oFSO.MoveFile "D:\S7_Export\Schrauber\????\*.*", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_Schrauber60R01").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schweißen kopieren 60R01
Public Sub HWH_60R01()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH60R01.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_60R01HwHNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH60R01.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_HWH60R01").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub


'Dateien vom Schweißen kopieren 60R02
Public Sub HWH_60R02()


Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object


strNrO = thisProject.Variables.Item("PDE_OrdnerName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "00000000000")
strDatei = Format(CStr(strNrD), "00000000000")


Set oFSO = CreateObject("Scripting.FileSystemObject")


If oFSO.FileExists("\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH60R02.txt") = False Then
    'File doesn't exist
thisProject.Variables.Item("PDE_60R02HwHNoFile").Value = 1
Else
    'File exist
oFSO.CopyFile "\\PEGASUS\AppData\" & strOrdner & "\" & strDatei & "HWH60R02.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If


thisProject.Variables.Item("PDE_HWH60R02").Value = 0 'Tiggersignal auf 0 setzen
         
Set oFSO = Nothing


End Sub
```

Über Projekt Add-Ins kann man auch in VB.Net oder in C# programmieren.




Da hätte ich aber das Problem das ich nicht weiß wie man bei Zenon ein Add-IN integriert und wie ich die S7 Variablen in den Online Container bekomme.
Dieses Zenon Projekt ist ohne Add-IN somit konnte ich mir für VB.Net nichts aneignen.

Deswegen denke ich die ganze Zeit daran es außerhalb von Zenon umzusetzen also mit einer anderen Software die Multitasking unterstützt.

Vielen Dank nochmal für deine Hilfe.

Mfg Tommylik


----------



## Thomas_v2.1 (22 Dezember 2020)

Es wäre erst einmal festzustellen, welche Aktion denn lange dauert.
Ein Umkopieren von Dateien und dann noch auf Netzwerkfreigaben könnte ein Punkt dafür sein. Vielleicht könnte das Umkopieren von einer anderen Anwendung außerhalb der Zenon Runtime vorgenommen werden. Aus deiner Runtime kopierst du die Daten nicht um, sondern teilst dieser Anwendung nur mit: bitte Datei von a nach b verschieben, dann löschen. Falls das mit dem Ablauf nicht passt, dann zumindest Dateien verschieben noch lokal, und das transferieren über Netzwerk macht dann die andere Anwendung.

Aber wie gesagt, ich würde mal in den existierenden Skripten ein Logfile mit Zeitstempeln schreiben, mit der Information wann eine Aktion gestartet und wann beendet wurde. Erst dann weißt du wo es hakt und wo es etwas zu optimieren gibt.


----------



## tommylik (22 Dezember 2020)

Hallo Thomas,

Vielen Dank für deine Antwort.

Gerade das umkopieren kann ich nicht mit einer anderen Anwendung machen,
 weil das Triggersignal mit einem String (Prägecode) verknüpft ist. 
Das Script sucht diesen String der den Anfang des Dateinamens bildet in dem angegebenen Ordner.
Die Quellen sind Netzwerkfreigaben das Ziel ist ein Ordner mit dem String als Ordnername.

Ich kenne keine andere Anwendung die ich an die S7 anbinden kann damit sie die Triggersignale und Strings empfangen kann
und dann Scripte ausführt. Zenon kann das. Aber blockiert die RT weil sie nicht Multitasking fähig ist. 
Deswegen wollte ich das mit einer anderen Software machen.


@ Larry



> Aber generell solltest du da eher Richtung .Net gehen (also z.B. VB.Net).



Ich habe heute nochmal mit dem Support von Zenon gesprochen.
Mit Add-In Framework ist es doch möglich da habe ich beim letzten mal etwas falsch verstanden. 
Es gibt in Zenon auch ein integriertes Visual Studio (VSTA) um in C# zu programmieren. Das wäre aber das gleiche was ich jetzt schon mit VBA habe.
Das würde auch die RT blockieren.
So ein Add-In läuft in einem eigenen Thread. Das Add-In kann man in VB.Net oder in C# programmieren.


Vielen Dank noch mal für die Hilfe.

Mfg Tommylik


----------



## JSEngineering (22 Dezember 2020)

Hallo Tommylik,

die Visualisierung läuft wo? Auf einem PC oder einem Panel?

Gibt es eine Möglichkeit, die ganze Tätigkeit direkt ins Betriebssystem zu schieben? Daß Du einen parallelen Task hast, den Du a) aufrufst oder b) triggerst, so daß der betriebssystemnah die Aufgabe erledigt? Denn das OS ist ja von sich aus multitasking fähig.

Wenn Du keine direkte Möglichkeit hast, Prozesse aufzurufen, wäre aber z.B. b) das Triggern möglich. Denn aus dem Script kannst Du bestimmt Dateien schreiben. Also Dein "Kopier"-Task läuft dauerhaft im Hintergrund auf niedriger Priorität und überwacht z.B. ein Verzeichnis. Sobald Du ihn benötigst, legst Du eine Datei mit den Parametern in das Verzeichnis. Der Task liest die aus und arbeitet entsprechend die Aufgabe ab.
Das sind natürlich keine hoch-respinsiven Möglichkeiten, aber vielleicht reicht es.

Somit könnte man ggf. auch im Betriebssystem den Tasks unterschiedliche Prioritäten geben...

Gruß
     Jens


----------



## tommylik (23 Dezember 2020)

Hallo Jens,

Vielen Dank für deine Antwort.

Also die Visualisierung läuft auf einem PC. 

Alles was mit dem Zenon Editor erstellt wird, muss übersetzt werden und dann an die Run Time übertragen werden.
Die RT bekommt die Triggersignale von der SPS und gibt an das Betriebssystem die Aufgaben weiter die Daten zu kopieren.
Von meiner Sicht aus kann ich nichts in das Betriebssystem schieben da ich die SPS für die Triggersignale brauche
und Zenon dieses Signale empfangen kann. Das Betriebssystem kann das nicht ohne eine Software. 

Ein Beispiel:

Roboter X macht einen Kleberauftrag. Dieser Kleberauftrag ist überwacht. Wenn der Roboter fertig ist wird eine Fertigmeldung gesetzt.
Mit dieser Fertigmeldung wird in der RT eine Funktion ausgelöst die das Script startet. 
Das Script sorgt dafür das von einem PC die Daten abgeholt werden und auf einem Server abgelegt werden.

Also wenn jemand von Euch eine andere Software kennt, die auch Triggersignale von einer S7 empfangen kann und dann in irgendeiner Form 
Windows mit teilen kann das Windows eine bestimmte Datei (String/Prägecode) von Punkt A nach B kopieren soll, wäre das super.

Vielen Dank nochmal für deine Hilfe.

Mfg Tommylik


----------



## Thomas_v2.1 (23 Dezember 2020)

Für die S7 gibt es etliche freie Bibliotheken um damit zu kommunizieren, libnodave, Snap7, und weitere, oder auch direkt fertige Module in diversen Programmiersprachen wie Python. Damit könntest du im Hintergrund ein paar Tasks aufsetzen die unabhängig von der Visualisierung das gewünschte machen könnten.

Aber in Punkto Wartbarkeit und Nachvollziehbarkeit würde es schon besser in die Visualisierung passen. Solche Skripte laufen dann gefühlt immer unter dem Radar, und werden bei einer Datensicherung auch gerne mal vergessen. Außerdem benötigst du dann zusätzliche Verbindungsressourcen in der SPS, das kann kein Problem sein, sollte man aber im Hinterkopf behalten. Außerdem möchtest du wenn deine zusätzlichen Skripte Probleme bereiten diese wohl auch in der Visualisierung zur Anzeige bringen, hier wäre dann eine weitere Schnittstelle notwendig.


----------



## Rabi (23 Dezember 2020)

Hallo tommylik,

ich habe bereits mehr mit ZenOn und hätte da 1-2 Vorschläge für dich. Da du hauptsächlich Werte in Dateien ablegst würde ich dir die zweite Variante empfehlen.


1. PCE (Process Control Engine), könnte dir eventuell helfen, findest du im ZenOn ebenfalls unter den Programmierschnittstellen. 
    Dort kannst du normalerweise mehrere Prozesse starten die dann parallel ablaufen, die Frage ist nur ob das für deinen Umfang reicht.

2. Du kannst über die Zenon.Interop.DLL mit einer normalen VB.NET Applikation direkt auf die Runtime Variablen zugreifen, mit dem vollem Umfang, also auch OnlineVariables.
    Damit könntest du dir eine kleine .exe Datei bauen die du dann als Datei in ZenOn einbindest und bei dem Application.Start immer mit startest.
    Auf deine laufende Runtime kannst du dich mit folgendem Code einfach verbinden:







Sobald du zur RT verbunden bist kannst du über die obZenon, die als ZenOn.Application deklariert wurde auf alle Funktionen, gleich wie im integrierten VBA, zugreifen.

So könntest du dir in der VB.NET Applikation eine TaskFactory erstellen mit der du asynchron mehrere Prozesse parallel ablaufen lassen kannst.


----------



## tommylik (24 Dezember 2020)

Hallo Thomas,

Vielen Dank für deine Antwort.

Von libnodave habe ich schon gehört in Zusammenhang mit Excel. 
Aber wenn ich das so überdenke was du geschrieben hast wird es mit einer Externen Software eher komplizierter.

Ich werde mich erstmal mit deine Vorschläge genauer befassen. Mal sehen ob das mir weiterhilft.

Vielen Dank nochmal für deine Hilfe.

Mfg Tommylik


----------



## tommylik (24 Dezember 2020)

Hallo Rabi,

Also von PCE habe ich noch nichts gehört. Und unter der Programmierschnittstelle ist sie auch nicht mit dabei.

Also dein 2 Vorschlag hört sich gut an. Von der Zenon.Interop.DLL habe ich mal was gelesen in Zusammenhang mit Add-In Framework.
Mit kleine .exe Datei bauen hast du mir einen schönen Schrecken eingejagt.
Aber gut, was benötige ich dafür? Das geht ja bestimmt nicht mit dem Zenon Editor.
Das hört sich aus deinem Munde sehr einfach an.
Wie sieht in meinem Fall der Aufbau aus. Wo muss ich den neuen OnlineContainer erstellen wenn ich das mit VB.Net mache?
Die Variablen von der SPS und die Makros wo wird das alles neu erstellt?
Wo kommt der Code hin aus deinem Screeshot?

Fragen über Fragen.

Zur Zeit habe ich das alles im thisprojekt in VBA. Kommt deine Idee in MyWorkspace?



Das klingt alles sehr interessant. Da muss ich mich erstmal in VB.net einlesen. z.B was eine TaskFactory ist.
Ich beneide Euch alle die richtig Programmieren können. Bei mir ist das alles Copy and Paste.

Vielen Dank noch mal für deine Hilfe.

Mfg Tommylik


----------



## tommylik (27 Dezember 2020)

Hallo Rabi,

Du hast mich mit deinen Vorschlägen weitergebracht könnte aber deine Hilfe gebrauchen da du dich ja anscheinend besser mit Zenon auskennst.

Zu deiner Info PCE gab es nur bis zur Version 7.20 ich habe 7.60 und ab dieser Version kann man mit Add-In Framework arbeiten. 
Laut dem Support arbeitet ein Add-In in seinem eigenen Thread und blockiert nicht die RT.

Ich habe dann mit Hilfe von

https://onlinehelp.copadata.com/help/760/addin/html/ObjectModel Help.htm

folgendes begonnen:



```
using System;
using System.Diagnostics;
using Scada.AddIn.Contracts;
using Scada.AddIn.Contracts.Variable;

namespace PDE
{
    /// <summary>
    /// Description of Project Service Extension.
    /// </summary>
    [AddInExtension("OnlineContainer", "Online Container erstellen")]
    public class ProjectServiceExtension : IProjectServiceExtension
    {
        #region IProjectServiceExtension implementation

        IProject PDE_Project = null;
        IOnlineVariableContainer myContainer = null;
        string onlineContainerName = "MyOnlineContainer";

        public void Start(IProject context, IBehavior behavior)
        {
            // enter your code which should be executed when starting the SCADA Runtime Service
            PDE_Project = context;

            if (PDE_Project == null)
            {
                Debug.Print("Reference to project is null.", DebugPrintStyle.Error);
                return;
            }

            string myVariableName1 = "PDE_PraegeCode";   //Stringvariable
            string myVariableName2 = "PDE_FolderName";   //Stringvariable
            string myVariableName3 = "PDE_CreateFolder"; //Boolvariable
            string myVariableName4 = "PDE_NoFolder";     //Boolvariable
            string myVariableName5 = "PDE_FileCopy";     //Boolvariable
            string myVariableName6 = "PDE_NoFile";       //Boolvariable

            if (PDE_Project.OnlineVariableContainerCollection[onlineContainerName] == null)
            {
                //create a new online Container
                myContainer = PDE_Project.OnlineVariableContainerCollection.Create(onlineContainerName);
                //add variable to the container
                myContainer.AddVariable(myVariableName1); //Stringvariable
                myContainer.AddVariable(myVariableName2); //Stringvariable
                myContainer.AddVariable(myVariableName3); //Boolvariable
                myContainer.AddVariable(myVariableName4); //Boolvariable
                myContainer.AddVariable(myVariableName5); //Boolvariable
                myContainer.AddVariable(myVariableName6); //Boolvariable

                myContainer.Changed += MyContainer_Changed;

                //activate the online container itself
                myContainer.Activate();

            }

        }


        private void MyContainer_Changed(object sender, ChangedEventArgs e)
        {
            Was kommt hier rein ?????????????????????????????????????????????????????????? 
        }

        public void Stop()
        {
            // enter your code which should be executed when stopping the SCADA Runtime Service


            myContainer.Deactivate();
            myContainer.Changed -= MyContainer_Changed;
            PDE_Project.OnlineVariableContainerCollection.Delete(this.onlineContainerName);

        }

        #endregion
    }
}
```

Könnte jetzt deine Hilfe gebrauchen wie ich folgende 2 Skripte mit eingebunden bekomme.
Es gibt leider keinen Converter von VBA nach C#.


```
Public Sub Ordner_erstellen()
'Diese Modul erstellt einen Ordner

Dim strNr As String
Dim strText As String
Dim strPfad As String
Dim strOrdner As String
Dim oFSO As Object

strNr = thisProject.Variables.Item("PDE_FolderName").Value
strText = Format(CStr(strNr), "000000000000") 'String 12 für den Ordnernamen

'Exportpfad
strPfad = "D:\S7_Export\AppData"     ' Pfad einstellen
strOrdner = strText
   
Set oFSO = CreateObject("Scripting.FileSystemObject")
        
        If oFSO.FolderExists(strPfad) Then
            If Not oFSO.FolderExists(strPfad & "\" & strOrdner) Then
                oFSO.CreateFolder (strPfad & "\" & strOrdner)
                
                'SPS Trigger rücksetzen.
                thisProject.Variables.Item("PDE_CreateFolder").Value = 0
            End If
            
        Else
        'SPS informieren wenn Exportpfad nicht vorhanden.
        thisProject.Variables.Item("PDE_NoFolder").Value = 1
        End If
        
 Set oFSO = Nothing
 
End Sub
```


```
Public Sub FileCopy()

Dim strNrO As Variant
Dim strNrD As Variant
Dim strOrdner As String
Dim strDatei As String
Dim oFSO As Object

strNrO = thisProject.Variables.Item("PDE_FolderName").Value
strNrD = thisProject.Variables.Item("PDE_PraegeCode").Value
strOrdner = Format(CStr(strNrO), "000000000000") 'String 12 für den Ordnernamen
strDatei = Format(CStr(strNrD), "00000000000")     'String 11 für den Dateinamen         

Set oFSO = CreateObject("Scripting.FileSystemObject")

'Fehlerroutine: Datei nicht vorhanden
If oFSO.FileExists("\\TOX210R01\AppData\" & strDatei & "TOX210R01.txt") = False Then
   'File doesn't exist
thisProject.Variables.Item("PDE_NoFile").Value = 1
Else
   'File exist
oFSO.CopyFile "\\TOX210R01\AppData\" & strDatei & "TOX210R01.txt", "D:\S7_Export\AppData\" & strOrdner & "\"
End If

thisProject.Variables.Item("PDE_FileCopy").Value = 0 'SPS Trigger rücksetzen.
         
Set oFSO = Nothing

End Sub
```

Ich hoffe du kannst mir helfen.

Mfg Tommylik


----------



## Blockmove (28 Dezember 2020)

Wir nutzen für solche Geschichten oft Node RED (kostenlos) oder Traeger CoDaBix.
Beide laufen auf diversen Plattformen (PC, Raspi, div.IoT-Gateways).


----------



## StephanKuhlmann (28 Dezember 2020)

Hi, wir nutzen für solche Sqchen ebenfalls NodeRed mit den entsprechenden Nodes für S7, das klappt immer gut. Speziell für Datenbankkommunikation mit Postgresql oder MSSql gibt es gute Nodes, mit denen sich das bewerkstelligen lässt. Und natürlich auch schreiben in Textdateien ist kein Problem. Das Ganze basiert auf Node.js und wird mit JavaScript programmiert bzw. auch oft ganz ohne Code. Einfach mal anschauen. Es gibt schon einige Hersteller, die das direkt in ihre Steuerungen einbauen. Grüße Stephan


----------



## tommylik (28 Dezember 2020)

Hallo,

Vielen Dank für Eure Antworten und die Vorschläge mit Note Red. 
Aber ich habe ja jetzt die Idee von Rabi das ich das mit der vorhandenen Software umzusetzen kann, ohne eine zusätzliche Software auf dem PC.

Aber trotzdem vielen Dank nochmal an Euch für Eure Hilfe.

Mfg Tommylik


----------



## Rabi (30 Dezember 2020)

Hallo,

über das Framework ist es natürlich noch etwas besser das ganze zu machen, ich bin noch die älteren Versionen von ZenOn gewohnt wo ich eben direkt auf den RT-Prozess zugreifen musste.

In deinem Fall würde ich dir empfehlen wenn du mit dem Samples von Copa-Data arbeitest, dafür brauchst du eben die Developer Tools die du vermutlich schon installiert hast.

Hier ansonsten die Developer-Tools:
https://marketplace.visualstudio.co...63468.COPA-DATASCADAAdd-InDeveloperToolsforVS

Und hier die AddIn-Samples:
https://github.com/COPA-DATA/AddInHowTo


Für mich sieht es so aus als ob du die Online-Container bereits richtig erstellt hast.



```
[COLOR=#3E3E3E][FONT=Courier]        private void MyContainer_Changed(object sender, ChangedEventArgs e)[/FONT][/COLOR]        {
            Was kommt hier rein ??????????????????????????????????????????????????????????  [COLOR=#3E3E3E][FONT=Courier]        }[/FONT][/COLOR]
```

Dieser Codeabschnitt ist das angemeldete Event für deine Online-Container und wird eben aufgerufen sobald sich eine Variable ändert die du dem Container zugewiesen hast.


Ich würde dir raten noch einmal mehr die Samples von Copa-Data anzusehen wie man richtig einen Container erstellt und Variablen zuweist.
Im Anhang findest du auch ein schönes Beispiel wie die TaskFactory zu verwenden ist damit eben die RT nicht gestoppt wird wenn etwas zu lange dauert.
Und genau bei der ValueSubscription im Bild würden deine Skript hineingehören, somit startest du immer sofort einen neuen Task wenn eine Variable sich ändert.
Das einzige Problem dass du haben könntest, da dort nicht auf das erfolgreiche Beenden eines Tasks gewartet wird, könntest du einen FileAccess-Error bekommen.
Aber am besten versuchst du es einfach mal 


Deine zwei VB Scripte schreibe ich dir so jetzt nicht um, aber ich kann die empfehlen dass du dich in das FileSystem vom C# einliest.
Eigentlich ist da nicht sehr viel dahinter und du solltest deine Scripte relativ zügig umschreiben schaffen.

Man wird eigentlich relativ schnell fündig wenn man ein bisschen danach googelt, sieht hier:
https://docs.microsoft.com/en-us/do...guide/file-system/how-to-write-to-a-text-file


----------



## tommylik (2 Januar 2021)

Hallo Rabi,

Vielen Dank für deine tolle Unterstützung.

Die Tools habe ich schon. Ich habe da noch ein paar Fragen. 

Du sagst das die Skripte in die ValueSubscription gehören. Warum? 

Was ist die ValueSubscription ?
Was ist Guid.NewGuid() ?

Ich dachte die Skripte gehören in das Change Event?


```
private void MyContainer_Changed(object sender, ChangedEventArgs e)
```

Vor allem was ich nicht verstehe ist das die ValueSubscription vor dem Start aufgerufen wird. Warum?


```
public VariableSubscription(Action<IEnumerable<IVariable>> variableChangeReceivedAction)
        {
            _variableChangeReceivedAction = variableChangeReceivedAction;
            _containerName = "MyOnlineContainerCollection-" + Guid.NewGuid();
        }


        public void Start(IProject context, IEnumerable<string> variables)
        {
            if (_project != null || _container != null)
            {
                throw new InvalidOperationException("Cannot start a new online container again.");
            }
```

In dem Beispiel von dem Bild wird das BulkChanged Event genutzt was ist dieses Bulk?
Wann sollte man das nutzen? 

Ich habe ja mit VBA einige Skripte erstellt. Wäre es sinnvoll für jedes Skript ein Add-In zu erstellen?
Mein Beispiel aus Post #14 ist ein Project Service Extension also läuft das Add-In immer sobald die RT gestartet wird.
In mein Fall was wäre die bessere Lösung ein Project Service Extension oder ein Project Wizard Extension?

Vielen Dank noch mal für deine Hilfe

Mfg Tommylik


----------



## Rabi (3 Januar 2021)

Um das ganze etwas zu vereinfachen, könntest du nochmal jetzt deinen Code hier reinstellen? Damit ich weiß mit was wir genau arbeiten.
Wenn wir jetzt zwei Versionen haben wird es etwas schwierig dir dabei zu helfen.

Genau deine Skripte gehören in deinem _Changed Event aufgerufen damit dort dann die Datei erstellt werden kann.

Du benötigst einen Project Service Extension. Du willst ja dass es mit der RT immer mitstartet.

Du könntest dir eine eigene statische Klasse erstellen in der du alle deine Skripte einzeln verpackst.
Somit könntest du immer bei dem auslösen des Events eine gewünschte Funktion ausführen.


----------



## tommylik (3 Januar 2021)

Hallo Rabi,

Vielen Dank für deine Antwort.

Gleich mal eine Rückfrage in deinem Post #18 hast du geschrieben das mein Skript in die ValueSubscription gehört jetzt sagst du ins _Changed Event. Ich bin verwirrt.

Nun gut, hier mein aktueller Stand den ich habe.


```
using Scada.AddIn.Contracts;
using Scada.AddIn.Contracts.Variable;
using System;
using System.IO;
using System.Collections.Generic;


namespace AddInOrdner_erstellen
{
    /// <summary>
    /// Description of Project Service Extension.
    /// </summary>
    [AddInExtension("Ordner_erstellen", "Einen Ordner mit Windows 10 erstellen")]
    public class ProjectServiceExtension : IProjectServiceExtension
    {
        #region IProjectServiceExtension implementation


        private readonly string _containerName;
        private IOnlineVariableContainer _container;
        private IProject _project;
               


        public void Start(IProject context, IBehavior behavior)
        {
            // enter your code which should be executed when starting the SCADA Runtime Service


            if (_project != null || _container != null)
            {
                throw new InvalidOperationException("Cannot start a new online container again.");
            }
          
            string VariableName1 = "PDE_PraegeCode";   //Stringvariable
            string VariableName2 = "PDE_FolderName";   //Stringvariable
            string VariableName3 = "PDE_CreateFolder"; //Boolvariable
            string VariableName4 = "PDE_NoFolder";     //Boolvariable
            string VariableName5 = "PDE_FileCopy";     //Boolvariable
            string VariableName6 = "PDE_NoFile";       //Boolvariable


            _project = context;


            try
            {
                // Ensure that the container is deleted
                context.OnlineVariableContainerCollection.Delete(_containerName);


                // Create a new container
                _container = context.OnlineVariableContainerCollection.Create(_containerName);


                // Add variables
                _container.AddVariable(VariableName1); //Stringvariable
                _container.AddVariable(VariableName2); //Stringvariable
                _container.AddVariable(VariableName3); //Boolvariable
                _container.AddVariable(VariableName4); //Boolvariable
                _container.AddVariable(VariableName5); //Boolvariable
                _container.AddVariable(VariableName6); //Boolvariable


                // register Event
                _container.Changed += _container_Changed;


                // Activate OnlineContainer
                _container.Activate();


            }
            catch (Exception)
            {
                
            }


        }


        private void _container_Changed(object sender, ChangedEventArgs e)
        {
            string strNr;
            string strText;
            string strPfad;
            string strOrdner;


            // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++      
            strNr = thisProject.Variables.Item("PDE_FolderName").Value;
            strText = Strings.Format((string)strNr, "000000000000"); // String 12 für den Ordnernamen
            // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


            // Exportpfad
            strPfad = @"D:\S7_Export\AppData";     // Pfad einstellen
            strOrdner = strText;


            try
            {
                if (Directory.Exists(strPfad))
                {
                    // Try to create the directory.
                    Directory.CreateDirectory(strPfad + "\\" + strOrdner);
                }
            }
            catch (Exception)
            {
                
            }


        }


        public void Stop()
        {
            // enter your code which should be executed when stopping the SCADA Runtime Service


            if (_container == null)
            {
                throw new InvalidOperationException("Stop() cannot be called before Start()");
            }


            // All events are removed here - the container gets disabled and deleted.


              _container.Changed -= _container_Changed;
               _container.Deactivate();

            
            try
            {
                _project.OnlineVariableContainerCollection.Delete(_containerName);
                _container = null;
                _project = null;
                
            }
            catch (Exception)
            {
                
            }
            
            
        }


        #endregion
    }
}
```

An dieser Stelle hänge ich:






Die beiden in Pluszeichen eingerahmte VBA Zeilen möchte in C# um basteln.
Aber ich denke anscheinend zu kompliziert und brauche das vielleicht gar nicht.


```
// Add variables
                _container.AddVariable(VariableName1); //Stringvariable
                _container.AddVariable(VariableName2); //Stringvariable
```



```
// Exportpfad
            strPfad = @"D:\S7_Export\AppData";     // Pfad einstellen
            strOrdner = [COLOR=#ff0000][B]An dieser Stelle brauche ich die VariableName2 das ist der Stringwert von der SPS den ich als Ordnername nutze.[/B][/COLOR]
```


Ich habe mir die AddInSampleLibrary genauer angeschaut und frage mich ob ich den logger (NLog) und den Error Handler unbedingt brauche.
Wenn es die RT nicht blockiert dann ist es mir egal aber wenn ich es weglassen kann dann umso besser.

Was ist eigentlich die variableChangeReceivedAction ???
Brauche ich noch die VariableSubscription am Anfang des Skriptes wenn ich jetzt das Change Event nutze ???


Vielen Dank nochmal für deine tolle Hilfe.

Mfg Tommylik


----------



## Rabi (3 Januar 2021)

Hallo,



> Gleich mal eine Rückfrage in deinem Post #18 hast du geschrieben das mein Skript in die ValueSubscription gehört jetzt sagst du ins _Changed Event. Ich bin verwirrt.



Ignoriere das bitte ich war da noch in einem anderen Beispiel drinnen - so ist es jetzt einfacher wenn wir mit deinem Source-Code arbeiten.


Ich habe leider keine Version höher als v7.60 installiert deswegen habe ich leider die nötigen .dll Dateien bzw. Bibliotheken nicht installiert und kann mir die Objekte daher nicht genau ansehen.
Aber ich versuche dir so gut wie möglich ein paar Dinge zu zeigen:

Erst zu dem ganze einfachen Dingen:


```
// Add variables                _container.AddVariable("PDE_PraegeCode"); //Stringvariable
                _container.AddVariable(VariableName2); //Stringvariable
                _container.AddVariable(VariableName3); //Boolvariable
                _container.AddVariable(VariableName4); //Boolvariable
                _container.AddVariable(VariableName5); //Boolvariable
                _container.AddVariable(VariableName6); //Boolvariable
```

Genau wie du gesagt hast wirst du die das deklarieren der Variablennamen als String sparen können und einfach direkt bei der Funktion übergeben können.
Dafür wäre nur wichtig ob die .AddVariable-Methode wirklich einen String annimmt oder eventuell das Variablen-Objekt benötigt.





> Ich habe mir die AddInSampleLibrary genauer angeschaut und frage mich ob ich den logger (NLog) und den Error Handler unbedingt brauche.


Das NLog kannst du eigentlich ganz entfernen, außer du möchtest für dich selbst ein paar Sachen mitloggen.
Den ErrorHandler kannst du ebenfalls entfernen da du sowieso die möglichen Fehler mit der try-catch Anweisung abfängst. Der würde dir vermutlich nur weitere Informationen liefern.




> Was ist eigentlich die variableChangeReceivedAction ???
> Brauche ich noch die VariableSubscription am Anfang des Skriptes wenn ich jetzt das Change Event nutze ???


Wie gesagt das nun bitte ignorieren ich habe mit einem anderen Sample gearbeitet als du .
Das benötigst du jetzt eben nicht weil du dein Event bereits in der Start-Methode abonnierst und somit das _container_Changed aufgerufen wird sobald sich eine Variable ändert.




```
[COLOR=#3E3E3E][FONT=Courier]strPfad = @"D:\S7_Export\AppData";     // Pfad einstellen
[/FONT][/COLOR][COLOR=#3E3E3E][FONT=Courier]            strOrdner = [/FONT][/COLOR][COLOR=#ff0000][FONT=Courier][B]An dieser Stelle brauche ich die VariableName2 das ist der Stringwert von der SPS den ich als Ordnername nutze.[/B][/FONT][/COLOR]
```
Ok, du hast jetzt eben bereits erfolgreich deinen Online-Container erstellt und ein Event abonniert und somit wird diese _container_Changed jetzt immer aufgerufen.
Leider habe ich, wie gesagt, die v7.60 nicht installiert und kann deswegen auf die Bibliotheken nicht zugreifen.

ABER damit du eben jetzt auf die Variablen zugreifen kannst übergibt dir das Event folgende EventArgs *ChangedEventArgs - diese deklarierst du als e im Methodenaufruf.
*Bedeutet du müsstest jetzt z.B: soetwas machen:

Ich würde zuerst oben noch eine interne Variable deklarieren mit deinen Variablennamen:

```
private readonly string _containerName;        private IOnlineVariableContainer _container;
        private IProject _project;
        string[] VariableNames = new string[] { "PDE_PraegeCode",   // ID: 0  RABI: Kurze Deklaration damit die Namen immer schön weiterverwendet werden können
                                                "PDE_FolderName",   // ID: 1
                                                "PDE_CreateFolder", // ID: 2
                                                "PDE_NoFolder",     // ID: 3
                                                "PDE_FileCopy",     // ID: 4
                                                "PDE_NoFile" };     // ID: 5
```


Dadurch kannst du das unten ganz schön abkürzen:

```
// Create a new container                _container = context.OnlineVariableContainerCollection.Create(_containerName);


                // RABI: Variablen mit einer kurzen Anweisung hinzufügen
                foreach (string n in VariableNames)
                {
                    _container.AddVariable(n);
                }
```

Dann würde ich dir empfehlen für alle längeren Prozeduren dir einfach unter der Stop-Methode noch deine ganzen größeren Skripte zu verpacken, hier nur ein Beispiel:

```
public void Stop()
        {
        }


        public void PraegeCodeCommand
        {
            // Hier musst du dann z.B.: Dateien erzeugen usw. alles was länger dauern könnte
        }
```

Das wichtigste kommt jetzt und zwar musst du noch diese Libary einbinden: using System.Threading.Tasks;
Danach kannst du innerhalb deiner Events z.B.: mit Hilfe der Tasks alles Asynchron starten und ebenfalls auswählen auf welche Skripte du wirklich warten möchtest.
Ganz oben siehst du noch dass ich für die Variable strnNr dann das Objekt _project verwendet habe, damit solltest du auf alles in deinem Projekt zugreifen können.


```
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++              strNr = _project.Variables.Item("PDE_FolderName").Value;
            strText = String.Format(strNr, "000000000000"); // String 12 für den Ordnernamen


            switch (e.Name)
            {
                case VariableNames(0): //PDE_PraegeCode
                    Task FirstTask = Task.Run(() => PraegeCodeCommand());
                    break;
                case VariableNames(1): //PDE_FolderName
                    Task SecondTask = Task.Run(() => PraegeCodeCommand());
                    break;
                case VariableNames(2): //PDE_CreateFolder
                    // Do something
                    break;
                case VariableNames(3): //PDE_NoFolder
                    // Do something
                    break;
                case VariableNames(4): //PDE_FileCopy
                    // Do something
                    break;
                case VariableNames(5): //PDE_NoFile
                    // Do something
                    break;
                default:
                    // No match
                    break;
            }
            Task.WaitAll(FirstTask, SecondTask);
            // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
```


----------



## tommylik (4 Januar 2021)

Hallo Rabi,

Man Super vielen Danke für deine tolle Hilfe.

Da du kein 7.60 hast konntest du nicht prüfen und hättest es gleich gesehen.
Diese Zeile funktioniert nicht. Das Wort Variables ist rot untersrichen und Item finde ich nicht in C# mit .Value am Ende.


```
strNr = _project.Variables.Item("PDE_FolderName").Value;
```


Wenn ich dieses so benutze wie du es hier als Beispiel angegeben hast sind alle VariableNames(x) rot unterstrichen.
e.Name gibt es nicht.



```
switch (e.Name)
            {
                case VariableNames(0): //PDE_PraegeCode
                    Task FirstTask = Task.Run(() => PraegeCodeCommand());
                    break;
                case VariableNames(1): //PDE_FolderName
                    Task SecondTask = Task.Run(() => PraegeCodeCommand());
```

Nur zu deiner Info diese beiden Variablen PDE_PraegeCode und PDE_FolderName sind String Variable und enthalten eine Nr. und sind nur für die Namen der Ordner und Dateien.
Nur mit den Bool Variablen kann man die Task Triggern.

Ich komme nicht so ganz klar mit der TaskFactory. Ich habe versucht es umzusetzen so wie ich deine Erklärungen verstanden habe.

Schau mal bitte was alles falsch ist ich hoffe nicht als zuviel.
Mein Stand jetzt:


```
using Scada.AddIn.Contracts;
using Scada.AddIn.Contracts.Variable;
using System;
using System.IO;
using System.Collections.Generic;
using System.Threading.Tasks;




namespace AddInOrdner_erstellen
{
    /// <summary>
    /// Description of Project Service Extension.
    /// </summary>
    [AddInExtension("Ordner_erstellen", "Einen Ordner mit Windows 10 erstellen")]
    public class ProjectServiceExtension : IProjectServiceExtension
    {
        #region IProjectServiceExtension implementation




        private readonly string _containerName;
        private IOnlineVariableContainer _container;
        private IProject _project;
        string[] VariableNames = new string[] { "PDE_PraegeCode",   // ID: 0  Kurze Deklaration damit die Namen immer schön weiterverwendet werden können
                                                "PDE_FolderName",   // ID: 1
                                                "PDE_CreateFolder", // ID: 2
                                                "PDE_NoFolder",     // ID: 3
                                                "PDE_FileCopy",     // ID: 4
                                                "PDE_NoFile" };     // ID: 5
        private Task[] FirstTask;
        private Task[] SecondTask;


        public void Start(IProject context, IBehavior behavior)
        {
            // enter your code which should be executed when starting the SCADA Runtime Service




            if (_project != null || _container != null)
            {
                throw new InvalidOperationException("Cannot start a new online container again.");
            }


            _project = context;


            try
            {
                // Ensure that the container is deleted
                context.OnlineVariableContainerCollection.Delete(_containerName);


                // Create a new container
                _container = context.OnlineVariableContainerCollection.Create(_containerName);




                // Add variables
                // Variablen mit einer kurzen Anweisung hinzufügen
                foreach (string n in VariableNames)
                {
                    _container.AddVariable(n);
                }


                // register Event
                _container.Changed += _container_Changed;


                // Activate OnlineContainer
                _container.Activate();


            }
            catch (Exception)
            {


            }


        }


        private void _container_Changed(object sender, ChangedEventArgs e)
        {
            
            switch (e.Variable.Name)


            {
                case "PDE_CreateFolder": //PDE_CreateFolder
                    Task FirstTask = Task.Run(() => CreateFolder());
                    break;
                case "PDE_FileCopy": //PDE_FileCopy
                    Task SecondTask = Task.Run(() => FileCopy());
                    break;
                default:
                    // No match
                    break;
            }

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            try
            {
                Task.WaitAll(FirstTask, SecondTask); [COLOR=#ff0000][B]Hier habe ich auch noch ein Problem FirstTask und SecondTassk sind rot unterstrichen.[/B][/COLOR]
            }


            catch (Exception)
            {


            }

[COLOR=#ff0000][B]Warscheinlich ist das hier an der falschen Stelle[/B][/COLOR]

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        }




        public void Stop()
        {
            // enter your code which should be executed when stopping the SCADA Runtime Service




            if (_container == null)
            {
                throw new InvalidOperationException("Stop() cannot be called before Start()");
            }


            




            // All events are removed here - the container gets disabled and deleted.




            _container.Changed -= _container_Changed;
            _container.Deactivate();




            try
            {
                _project.OnlineVariableContainerCollection.Delete(_containerName);
                _container = null;
                _project = null;


            }
            catch (Exception)
            {


            }




        }




        public void CreateFolder()
        {
            // Hier musst du dann z.B.: Dateien erzeugen usw. alles was länger dauern könnte


            string strPfad;
            string strOrdner;


            // Exportpfad
            strPfad = @"D:\S7_Export\AppData";     // Pfad einstellen
            strOrdner = "PDE_FolderName";




            try
            {
                if (Directory.Exists(strPfad))
                {
                    // Try to create the directory.
                    Directory.CreateDirectory(strPfad + "\\" + strOrdner);
                }
            }
            catch (Exception)
            {


            }




        }


        public void FileCopy()
        {
            // Hier musst du dann z.B.: Dateien erzeugen usw. alles was länger dauern könnte
        }


        #endregion
    }
}
```


Was mir noch fehlt wie ich mit C# an die SPS Signale zurück schicken kann.
Das ist sehr wichtig oder ich habe ein Problem mit dem SPS Programm.

So hatte ich es mit VBA gelöst.


```
'SPS Trigger rücksetzen wenn Ordner erstellt wurde.
thisProject.Variables.Item("PDE_CreateFolder").Value = 0
```

Oder wenn eine Datei nicht existiert dann möchte ich es der SPS melden.

```
If oFSO.FileExists("\\TOX210R01\AppData\" & strDatei & "TOX210R01.txt") = False Then
   'File doesn't exist
thisProject.Variables.Item("PDE_NoFile").Value = 1
```


Vielen Dank für deine tolle Hilfe und Mühe.

Mfg Tommylik


----------



## Rabi (5 Januar 2021)

Hallo,

zuerst kann ich dein Problem mit den VariableNames leider nicht nachvollziehen, in meinem Visual Studio funktioniert das einwandfrei auch wenn ich deinen gesamten Code bei mir reinkopiere.
Das müsstest du bitte selbst finden.

Ich konnte noch feststellen dass du die Tasks noch einmal separat deklariert hast, das ist bei meiner Definition gar nicht nötig.



Bei allen anderen Problemen würde ich dir raten du schaust dir das Object Model etwas genauer an.
Dort findest du relativ schnell wie man eine Variable deklarieren kann und diese kannst du dann zuweisen.

Siehe folgendes Interface:
https://onlinehelp.copadata.com/help/760/addin/html/Variable-IVariable.htm


Und hier siehst du eben noch das ChangedEventArgs das die Property als Variable drinnen hat:
https://onlinehelp.copadata.com/help/760/addin/html/Variable-ChangedEventArgs-Properties.htm


Im Variablen-Interface siehst du dann dass es einen Value gibt. Diesen kannst du eigentlich gleich wie in VBA setzen.


----------



## Rabi (7 Januar 2021)

Hallo tommy,

ich habe heute mal kurz Zeit gefunden und habe mir das ganze nochmal angesehen.

Testen müsstest du es selbst, kann aber nicht mehr so viel falsch sein.

Btw: Nachdem du auch im C# Forum angefragt hast, hätte ich dir gleich sagen können dass dir niemand das umschreiben abnimmt .


```
using System;using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using Scada.AddIn.Contracts;
using Scada.AddIn.Contracts.Variable;


namespace VariableSubscription_PDE
{
    public class TommyClass
    {
        private readonly string _containerName;
        private IOnlineVariableContainer _container;
        private IProject _project;


        private string ExportPath, FolderName, Code;
        private IVariable[] ZenonVariables = new IVariable[10];


        public TommyClass()
        {
            _containerName = "TommyOnlineContainer-" + Guid.NewGuid();
        }


        public void Start(IProject context, IEnumerable<string> variables)
        {
            _project = context;
            InitializeConfiguration();


            // Ensure that the container is deleted
            context.OnlineVariableContainerCollection.Delete(_containerName);


            // Create a new container
            _container = context.OnlineVariableContainerCollection.Create(_containerName);


            // Add variables and register Event
            try
            {
                foreach (IVariable n in ZenonVariables)
                {
                    _container.AddVariable(n.Name);
                }
                _container.Changed += Container_SingleChanged;
                _container.BulkChanged += Container_BulkChanged;
                _container.ActivateBulkMode();
                _container.Activate();
            }
            catch (Exception ex)
            {


            }
        }


        public void Stop()
        {
            // All events are removed here - the container gets disabled and deleted.
            _container.Changed -= Container_SingleChanged;
            _container.BulkChanged -= Container_BulkChanged;


            _container.Deactivate();
            try
            {
                _project.OnlineVariableContainerCollection.Delete(_containerName);
            }
            catch (Exception ex)
            {


            }


        }




        /// <summary>
        /// Here you can change the functions a little bit 
        /// </summary>
        public void InitializeConfiguration()
        {
            // define the variables (you can also use custom names if you want)
            ZenonVariables[0] = _project.VariableCollection["PDE_PraegeCode"];
            ZenonVariables[1] = _project.VariableCollection["PDE_FolderName"];
            ZenonVariables[2] = _project.VariableCollection["PDE_CreateFolder"];
            ZenonVariables[3] = _project.VariableCollection["PDE_NoFolder"];
            ZenonVariables[4] = _project.VariableCollection["PDE_FileCopy"];
            ZenonVariables[5] = _project.VariableCollection["PDE_NoFile"];


            // configure all the variables
            ExportPath = @"D:\S7_Export\AppData";
            FolderName = String.Format((string)ZenonVariables[1].GetValue(0), "############");
            Code = String.Format((string)ZenonVariables[0].GetValue(0), "###########");
        }


        public void CheckTaskExecution(string VariableName)
        {
            if (VariableName == ZenonVariables[2].Name) // CreateFolder
            {
                Task TaskCreateFolder = Task.Run(() => CreateFolder());
            }
            else if (VariableName == ZenonVariables[4].Name) //FileCopy
            {
                Task TaskFileCopy = Task.Run(() => FileCopy());
            }
        }


        public void CreateFolder()
        {      
            try
            {
                DirectoryInfo FolderInfo = Directory.CreateDirectory(ExportPath + "\\" + FolderName);
                if (FolderInfo.Exists)
                {
                    ZenonVariables[2].SetValue(0, 0); // Folder successfully created
                }
                else
                {
                    ZenonVariables[3].SetValue(0, 1); // No Folder existing
                }
            }
            catch (Exception)
            {
            }
        }


        public void FileCopy()
        {
            try
            {
                if (File.Exists($"\\\\TOX210R01\\AppData\\{Code}TOX210R01.txt"))
                {
                    ZenonVariables[5].SetValue(0, 1);
                }
                else
                {
                    File.Copy($"\\\\TOX210R01\\AppData\\{Code}TOX210R01.txt", $"D:\\S7_Export\\AppData\\{FolderName}\\");
                    ZenonVariables[4].SetValue(0, 0);
                }
            }
            catch (Exception ex)
            {
            }
        }


        private void Container_BulkChanged(object sender, BulkChangedEventArgs e)
        {
            foreach(IVariable n in e.Variables)
            {
                CheckTaskExecution(n.Name);
            }
        }


        private void Container_SingleChanged(object sender, ChangedEventArgs e)
        {
            CheckTaskExecution(e.Variable.Name);
        }
    }
}
```


----------



## tommylik (8 Januar 2021)

Hallo Rabi,

Vielen Dank für deine Hilfe und Zeit. Ja das mit dem Forum war eine blöde Idee. Da ich C# nun mal nicht kann war es ein Akt der Verzweiflung.
Du hattest mir ja bestätigt das ein Project Service Extension das beste wäre so habe ich deinen Code in ein neues Service Extension Projekt hinein kopiert.

Außer ein paar Deklarationen habe ich erstmal nichts geändert.

Um das ganze besser zu verstehen habe ich noch ein paar Fragen die sich jetzt durch die neue Strucktur ergeben haben.

Auf dem folgenden Bild siehst du das rot unterstrichende.




Das liegt daran das du den Start so programmiert hast:


```
public void Start(IProject context, IEnumerable<string> variables)
```

Wenn man es aber so schreibt geht der Fehler weg.

```
public void Start(IProject context, IBehavior behavior)
```

Hier siehst du das variables nicht genutzt wird.



Für was brauche ich das? ich verstehe das mit dem Guid nicht.

```
_containerName = "TommyOnlineContainer-" + Guid.NewGuid();
```

Dann eine gravierende Frage warum hast du vom Change Event auf InitializeConfiguration gewechselt?
Brauche ich dann noch die beiden Change Events Single und Bulk?

Wie wird diese Methode aufgerufen?


```
public void CheckTaskExecution(string VariableName)
```


Wann benutzt man das @ Zeichen und wann das $ Zeichen?

```
ExportPath = @"D:\S7_Export\AppData";

File.Copy($"\\\\TOX210R01\\AppData\\{strDatei}TOX210R01.txt", $"D:\\S7_Export\\AppData\\{strOrdner}\\");
```




```
using Scada.AddIn.Contracts;
using Scada.AddIn.Contracts.Variable;
using System;
using System.IO;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;




namespace AddInOrdner_erstellen
{
    /// <summary>
    /// Description of Project Service Extension.
    /// </summary>
    [AddInExtension("Name des AddIns", "Beschreibung des AddIns")]
    public class ProjectServiceExtension : IProjectServiceExtension
    {
        #region IProjectServiceExtension implementation




        private readonly string _containerName;
        private IOnlineVariableContainer _container;
        private IProject _project;


        private string ExportPath, strOrdner, strDatei;
        private IVariable[] ZenonVariables = new IVariable[10];


        public ProjectServiceExtension()
        {
            _containerName = "TommyOnlineContainer-" + Guid.NewGuid();
        }


        public void Start(IProject context, IEnumerable<string> variables)
        {
            // enter your code which should be executed when starting the SCADA Runtime Service


            _project = context;
            InitializeConfiguration();


            // Ensure that the container is deleted
            context.OnlineVariableContainerCollection.Delete(_containerName);


            // Create a new container
            _container = context.OnlineVariableContainerCollection.Create(_containerName);


            // Add variables and register Event
            try
            {
                foreach (IVariable n in ZenonVariables)
                {
                    _container.AddVariable(n.Name);
                }
                _container.Changed += Container_SingleChanged;
                _container.BulkChanged += Container_BulkChanged;
                _container.ActivateBulkMode();
                _container.Activate();
            }
            catch (Exception)
            {


            }


        }


        public void Stop()
        {
            // enter your code which should be executed when stopping the SCADA Runtime Service


            // All events are removed here - the container gets disabled and deleted.
            _container.Changed -= Container_SingleChanged;
            _container.BulkChanged -= Container_BulkChanged;


            _container.Deactivate();
            try
            {
                _project.OnlineVariableContainerCollection.Delete(_containerName);
            }
            catch (Exception)
            {


            }


        }


        /// <summary>
        /// Here you can change the functions a little bit 
        /// </summary>
        public void InitializeConfiguration()
        {
            // define the variables (you can also use custom names if you want)
           
            ZenonVariables[0] = _project.VariableCollection["PDE_FolderName"];
            ZenonVariables[1] = _project.VariableCollection["PDE_PraegeCode"];
            ZenonVariables[2] = _project.VariableCollection["PDE_CreateFolder"];
            ZenonVariables[3] = _project.VariableCollection["PDE_NoFolder"];
            ZenonVariables[4] = _project.VariableCollection["PDE_FileCopy"];
            ZenonVariables[5] = _project.VariableCollection["PDE_NoFile"];


            // configure all the variables
            ExportPath = @"D:\S7_Export\AppData";
            // String 12 für den Ordnernamen
            strOrdner = String.Format((string)ZenonVariables[0].GetValue(0), "############");
            // String 11 für den Dateinamen         
            strDatei = String.Format((string)ZenonVariables[1].GetValue(0), "###########");
            
        }


        public void CheckTaskExecution(string VariableName)
        {
            if (VariableName == ZenonVariables[2].Name) // CreateFolder
            {
                Task TaskCreateFolder = Task.Run(() => CreateFolder());
            }
            else if (VariableName == ZenonVariables[4].Name) //FileCopy
            {
                Task TaskFileCopy = Task.Run(() => FileCopy());
            }
        }


        public void CreateFolder()
        {
            try
            {
                DirectoryInfo FolderInfo = Directory.CreateDirectory(ExportPath + "\\" + strOrdner);
                if (FolderInfo.Exists)
                {
                    ZenonVariables[2].SetValue(0, 0); // Folder successfully created
                }
                else
                {
                    ZenonVariables[3].SetValue(0, 1); // No Folder existing
                }
            }
            catch (Exception)
            {
            }
        }


        public void FileCopy()
        {
            try
            {
                if (File.Exists($"\\\\TOX210R01\\AppData\\{strDatei}TOX210R01.txt"))
                {
                    ZenonVariables[5].SetValue(0, 1);
                }
                else
                {
                    File.Copy($"\\\\TOX210R01\\AppData\\{strDatei}TOX210R01.txt", $"D:\\S7_Export\\AppData\\{strOrdner}\\");
                    ZenonVariables[4].SetValue(0, 0);
                }
            }
            catch (Exception)
            {
            }
        }




        private void Container_BulkChanged(object sender, BulkChangedEventArgs e)
        {
            foreach (IVariable n in e.Variables)
            {
                CheckTaskExecution(n.Name);
            }
        }




        private void Container_SingleChanged(object sender, ChangedEventArgs e)
        {
            CheckTaskExecution(e.Variable.Name);
        }


        #endregion
    }
}
```


Nochmals vielen vielen Dank für deine super Hilfe.

Mfg Tommylik


----------



## Rabi (9 Januar 2021)

Hallo,

was den Fehler angeht den du erwähnt hast wird vermutlich zustande gekommen sein weil ich die neueste zenOn Version verwendet habe, dadurch ist die API etwas anders .
Sollte dann schon passen so wie du es ausgebessert hast.




```
[COLOR=#3E3E3E][FONT=Courier]_containerName = "TommyOnlineContainer-" + Guid.NewGuid();[/FONT][/COLOR]
```

Die GUID generierst du dir eigentlich zur zu deinem ContainerNamen dazu damit dieser ganz sicher eindeutig ist.
Das macht man normalerweise auch für COM Objekte damit sie sicher eine eindeutige ID haben. Du kannst die Funktion auch über das Visual Studio manuell ausführen.
Damit wird eine ziemlich lange "zufällige" ID erzeugt. Somit kannst du sichergehen dass der ContainerNamen nicht doppelt existiert.



```
[COLOR=#3E3E3E][FONT=Courier]public void CheckTaskExecution(string VariableName)[/FONT][/COLOR]
```

Bräuchtest du jetzt nur suchen in dem Projekt. Die Funktion wird ausgeführt wenn ein Event _Changed, _BulkChanged ausgeführt wird.
Somit brauchst du nicht bei beiden Events immer dieselben Änderungen machen sondern nur einmal gezielt in dieser Funktion.



Beim @ und $ gibt es einen einfachen Unterschied:

```
[COLOR=#3E3E3E][FONT=Courier]File.Copy($"\\\\TOX210R01\\AppData\\{strDatei}TOX210R01.txt", $"D:\\S7_Export\\AppData\\{strOrdner}\\");[/FONT][/COLOR]
```

Wenn du eben z.B.: eine Ordnerstruktur schreibst musst du ja "" verwenden. Du hast das einfach gemacht indem du vor deinem Text ein @ gesetzt hast damit wird das dann immer richtig interpretiert.
Wenn du das @ aber nicht verwendest musst du immer für dieses Sonderzeichen noch ein "" vorraussetzen. Dadurch sieht die Struktur jetzt aufgebalsen auf mit "\\". Somit gesamt nur zwei.

Wenn man ein $ Zeichen vor den Wert setzt kann man direkt im String Variablen verwenden, siehst du hier mit {strDatei}.
Wenn du das nicht verwendest müsstest du es so schreiben:


```
[COLOR=#3E3E3E][FONT=Courier]File.Copy($"\\\\TOX210R01\\AppData\\{0}TOX210R01.txt", $"D:\\S7_Export\\AppData\\{1}\\", strDatei, strFolder);[/FONT][/COLOR]
```


----------



## tommylik (9 Januar 2021)

Hallo Rabi,

Vielen Dank für deine Antworten.

Ok habe ich tatsächlich nicht richtig darauf geachtet.


Und diese Methoden werden aufgerufen wenn sich eine Variable ändert??

```
private void Container_SingleChanged(object sender, ChangedEventArgs e)
        {
            CheckTaskExecution(e.Variable.Name);
        }
```


Und die ruft dann diese Methode auf??


```
public void CheckTaskExecution(string VariableName)
```


Vielen Dank nochmal für deine Antworten und ausführliche Hilfe.


Mfg Tommylik


----------



## Rabi (9 Januar 2021)

Hallo, 

ja genau so ist es.

Der Unterschied ist eben wichtig zu erkennen, das _SingleChanged Event liefert dir wirklich eine einzelne Variable zurück wenn sie sich ändert.

Das _BulkChanged Event liefert dir alle Variablen zurück wenn sich etwas ändert. Wirst du vermutlich gar nicht brauchen .


In der CheckTaskExecution werden eben die Variablennamen geprüft und anhand von denen die richtrigen Tasks gestartet damit etwas ausgeführt wird.


----------



## tommylik (9 Januar 2021)

Hallo,

Aber wenn sich z. B. mehrere Variablen gleichzeitig ändern würden dann wäre es besser das _BulkChanged Event zu nutzen??

Viele Task könnte man den gleichzeitig aufrufen?? Ohne das es die RT beeinflusst??


Vielen Dank für deine Hilfe


Mfg Tommylik


----------



## Rabi (9 Januar 2021)

Du müsstest versuchen wie genau der EventHandler funktioniert, leider bekomme ich vom Object Model dafür keine genauere Information.

Ich denke das Event löst nur einmal aus wenn sich mehrere Variablen in deinem OnlineContainer im gleichen Moment ändern. 
Dann kannst du alle abfangen und in einem Aufruf abarbeiten.

Das normale _Changed Event wird für jede einzelne Variable aufgerufen.


Da du ja nur 2 Variablen verwendest auf die du immer reagieren musst reicht dir sicherlich der normale _SingleChanged.
Der BulkMode macht eigentlich nur Sinn wenn du wirklich sehr sehr viele Variablen hast die gleichzeitig den Wert ändern könnten.

Wie viele Tasks gleichzeitig gestartet werden können hängt von deiner Prozessorleistung und dem Vorgang innerhalb der Tasks ab.
In deinem Fall sind die Tasks ja sofort wieder beendet da keine langen Prozeduren durchlaufen werden. Eine Datei ist sofort erstellt und auch sofort kopiert.

Mit deinem Programm sollten auch mehrere 100 Tasks parallel laufen können ohne dass du etwas davon merkst.


----------



## tommylik (12 Januar 2021)

Hallo Rabi,

Vielen Dank für deine Antwort.

Ich habe das AddIn mal ausprobiert. Es funktioniert nicht. 
Ich kann aber auch nicht sagen ob das Projekt Service Extension überhaupt läuft. 
Man sieht ja gar nichts.
Wie kann ich überprüfen ob das Extension mit der RT mit geladen wurde.
Wie kann ich überhaupt prüfen ob der Code funktioniert.

Ich habe einfach mal auf die Play Taste gedrückt im Visual Studio und habe in der Ausgabe folgendes bekommen:




So wie ich das verstehe blockiert sich da was gegenseitig.


Mfg Tommylik


----------



## tommylik (13 Januar 2021)

Hallo Rabi,

Ich habe noch nicht viel erreicht aber so viel das folgendes fehlte:


```
DefaultStartMode=DefaultStartupModes.Auto
```

Damit der Service automatisch startet.

Dann habe ich mal das ausprobiert:


```
// configure all the variables
            ExportPath = @"D:\S7_Export\AppData";
            // String 12 für den Ordnernamen
            strOrdner = String.Format((string)SPSVariables[1].GetValue(0), "############");


            MessageBox.Show(strOrdner);
            MessageBox.Show(ExportPath);
```

Die MessageBox hat schonmal funktioniert somit ist klar das der OnlineContainer funktioniert.
Und die richtigen Variable wurden auch übergeben.

Was noch nicht funktioniert ist das der Ordner erstellt wird.
Weil der Service nicht dauerhaft läuft. 

Um das zu Testen musste ich den Service manuell über einen Button starten.

Was fehlt hier noch?

Mfg Tommylik


----------



## Rabi (14 Januar 2021)

Hallo,

ich habe festgestellt dass anscheinend die Start-Funktion früher aufgerufen wird als die Variablen initialisiert sind. 
Dadurch läufst du relativ früh in deine Exception. Ebenfalls hab ich mit dem Foreach einen kleinen Gedankenfehler gehabt und habe dort jetzt die Objekte auf != null geprüft.

Im Anhang findest du die gesamte Extension. Das erstellen der Ordner habe ich getestet, für Testzwecke habe ich die Messageboxes noch drinnen gelassen.

Ev. kannst du damit noch etwas anfangen.


----------



## tommylik (14 Januar 2021)

Hallo Rabi,

Super vielen Dank für deine tolle Unterstützung.

Wenn du mal Zeit hast kannst du mir das mit der VariableSubscription erklären. Wofür ist die.
Du hattest ja am Anfang schon gesagt das es mit einer Klasse am Besten wäre.
Wie Hängen diese beiden Skripte zusammen ProjectServiceExtension.cs und TommyClass.cs??
Wie ist von der Reihenfolge der Ablauf. In beiden Dateien ist eine Start Methode.

Vielen Dank nochmal für deine super Hilfe.

Mfg Tommylik


----------



## tommylik (16 Januar 2021)

Hallo Rabi,

Also der Service ist zwar jetzt gestartet aber es passiert nichts wenn ich z. B. den Trigger für Ordner erstellen setze.
Dann ist mir aufgefallen das die RT sehr sehr lange brauch bis sie gestartet ist. >1 Min. Vorher war sie in ca. 20s gestartet. 
Ist das so wenn AddIns mit an Board sind das die RT langsamer startet oder deutet das noch auf einen Fehler hin??

Dann wäre es gut wenn wir mal abgleichen wie Du die Variablen in der SPS nutzt und wie Du Sie in Zenon angelegt hast.
Denn du sagtest das es bei dir funktioniert bei mir leider nicht.

So sind sie bei mir angelegt.




Die SPS funktioniert bei mir so:

Mit einem Merker wird aus einem Typverwaltungs-DB mit 2 BLKMOV der Prägecode in 2 Strings in den DB2280 kopiert.
Und für den Ordnername noch ein L oder R extra. Deswegen String 12 für den Ordnernamen.
Nach einer kurzen ASV wird der Merker gesetzt der dann das Ordnererstellen auslösen soll.
Ist der Ordner erstellt setzt Zenon den Merker wieder zurück. Wenn nicht gibt es einen Hinweis mit PDE_NoFolder.

Vielen Dank nochmal für deine Hilfe.

Mfg Tommylik


----------



## Rabi (18 Januar 2021)

Hallo,

wie gesagt du müsstest nochmal prüfen ob das Object Model von der 8.20 (die ich verwende) und der 7.60 (die du hast) ident sind.
Sonst könnte es sein dass wir bereits beim definieren von irgendwelchen Methoden in den try-catch Block laufen.

Jetzt wird es für mich leider sehr schwierig das Problem so ausfindig zu machen.
Bitte prüfe noch einmal ob deine Variablen wirklich alle auf "Extern Visible" gestellt sind unter den Externen Einstellungen jeder Variable.

Ggf. könntest du noch prüfen welche diese Eigenschaft alle haben, da zu Start der Runtime alle Variablen mit dieser Eigenschaft automatisch in die VariableCollection geschoben werden.
Das könnte eventuell deinen RT-Start verzögern, oder eben die falschen Aufrufe in deinem AddIn.


Ich würde dir dazu empfehlen dass du das Kapitel "Debugging" für die AddIns in dem Copa-Data Handbuch anschaust und prüfst wo genau das AddIn nicht mehr funktioniert:
http://download.copadata.com/filead...GERMAN/Handbuch/Programmierschnittstellen.pdf
P.S.: Du kannst auch ein VBA AddIn erstellen, wäre für dich vielleicht einfacher da du ja mit C# nicht wirklich Erfahrung hast (findest du ebenfalls beim Projekt erstellen unter SCADA).
Dort kannst du das gewohnte VBA Object Model verwenden. Auch wenn das VBA und C# Object Model fast ident sind, gibt es doch kleine aber feine Unterschiede.

Wie genau der Ablauf von deiner Steuerung ist, sollte in diesem Sinne relativ egal sein.


Zwecks der vorherigen Frage:
Die ProjectExtension ist einfach eine Klasse die vom Interface IProjectServiceExtension erbt. Würde die nicht implementiert sein würde das AddIn einfach nicht aufgerufen werden.
Innerhalb dieser Klasse wird eben dann die Klasse "TommyClass" deklariert und dementsprechend Werte zugewiesen.
Ebenfalls abonniert die TommyClass auch noch das Event "_Changed" und "_BulkChanged" von der Klasse ProjectExtension.

Hier wird es bald kompliziert das ganze zu erklären weil dir dort leider das Grundwissen fehlt was genau ein Event ist und wieso es abonniert werden muss.


Hoffe ich konnte dir trotzdem noch etwas weiterhelfen.


----------



## tommylik (18 Januar 2021)

Hallo Rabi,


Vielen Dank für deine Antwort.


Also wenn es für dich jetzt schon schwer wird dann gute Nacht für mich.
Wie kann ich den das Object Model 7.60 mit 8.20 vergleichen ich habe ja kein 8.20?

Ok habe ich gefunden Siehe unten.


Ich habe alle Variablen auf Extern Visible gestellt keine Änderung mit dem Laden immer noch sehr langsam.
Also die Eigenschaften der Variablen sind soweit gleich bis auf den Unterschied zwischen den beiden String Variablen den vier Bool Variablen.


Das Debugging werde ich mir mal anschauen.
Also jetzt das ganze von vorne mit VB.net muss nicht sein. Vor allem gibt es da keine Beispiele ProjectServiceExtension von Copa Data.


Ich habe mal Debuggen gestartet kannst du mit diesen Fehlermeldungen etwas anfangen??






Du hast anscheinend Recht das eine Methode oder mehrere in den try-catch Block laufen.


Weil im I-Net steht das 0x80070057 darauf Hinweist das Eigenschaften oder Argumente nicht stimmen.


Ein Beispiel AddIn Beispiel für 7.60


https://onlinehelp.copadata.com/help/760/addin/html/Variable-IOnlineVariableContainer-Changed.htm


und eins für 8.20


https://onlinehelp.copadata.com/help/820/addin/html/Variable-IOnlineVariableContainer-Changed.htm


Ich werde mal versuchen das Beispiel von 7.60 umsetzen vielleicht erstmal ohne Task.


Hier gibt es auch noch ein Beispiel mit Task für die 7.60.


https://onlinehelp.copadata.com/help/760/addin/html/Variable-IOnlineVariableContainerCollection.htm




Vielen Dank nochmal für deine Hilfe.


Mfg Tommylik


----------

