Brauche Unterstützung bei Modbus

  • VB.NET

Es gibt 41 Antworten in diesem Thema. Der letzte Beitrag () ist von hal2000.

    sonne75 schrieb:

    wie ich ein Element wieder an Anfang der Queue hinzufüge
    Das kommt da nicht vor, es ist halt eine Queue.
    Wenn Du hi und da was reinschieben willst, musst Du eine List(Of T) nehmen, das ist aber nicht Sinn der Übung.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Ui, da sind ja einige Rückfragen aufgelaufen. Ich fang mal oben an:

    Callback: Ja, das hat mit AddressOf zutun. Du übergibst einen Delegaten (Funktionszeiger), der "später" aufgerufen wird (wenn das Element abgearbeitet wurde).
    Synchronisationsobjekt: Du musst irgendwo auf Daten warten. Stell dir vor, du übergibst dem Scheduler 1000 Aufträge und möchtest das Ergebnis des 500sten haben. Das dauert aber, weil erstmal 499 andere Aufträge abgearbeitet werden müssen. Der Empfangspuffer der Transaktion ist also direkt nach dem Einreihen in die Warteschlange noch ungültig. Da kommt die Synchronisation ins Spiel: Der Thread, der das 500ste Ergebnis haben will, wartet auf das Synchronisationsobjekt (oft ein AutoResetEvent) in der Transaktion, bis der Scheduler die Transaktion als abgeschlossen signalisiert. Ab diesem Zeitpunkt ist sichergestellt, dass die Daten gültig sind.

    Polling funktioniert genauso wie einfache Befehle: Der Scheduler wird mit Anfragen geflutet - und zwar etwa mit so vielen, dass das Ergebnis mit ziemlich Sicherheit zurückkommt. Wenn nicht, weitere Anfrage queuen, sonst eben nicht. Das hat 2 Nachteile: 1. Die Queue ist stark belastet, 2. Du bekommst ggf. doppelte / mehrfache gültige Ergebnisse. Besser ist es, eine Polling-Transaktion zu erstellen, die den Scheduler intern zum Pollen veranlasst, bis ein gültiger Wert zurückkommt. Das hat wiederum den Nachteil, dass der Scheduler währenddessen keine anderen Anfragen bearbeiten kann - je nach Anwendungsgebiet und Gerät ist das eine oder andere Verfahren besser.

    Timer: Schlecht. Benutze einen separaten Thread und lass ihn entsprechend lange zwischendurch schlafen. Timer haben folgendes Problem: Wenn die ausgelöste Aktion ausnahmsweise länger als das Timer-Intervall dauert, gibts Chaos.

    Queue(T): Kann man benutzen, aber die musst du selbst synchronisieren. Im Framework 4 gibt es eine threadsichere Variante, die das automatisch macht: BlockingCollection(T).

    List(T): Eine Möglichkeit, wenn du Prioritäten verwalten musst. Auch diese musst du manuell synchronisieren.

    Hier ist der grundsätzliche Aufbau (wie schon beschrieben):

    Scheduler:

    Quellcode

    1. ----->
    2. | | --> Take transaction object from Queue
    3. | | --> Call callback (async)
    4. | | --> Do transaction
    5. | | --> Write result buffer
    6. | | --> Signal synchronization object
    7. | | --> Call callback (sync)
    8. <-----

    Das Callback wird nur einmal aufgerufen - entweder synchron (wenn das Ergebnis vorliegt) oder asynchron (wenn der Empfänger auf das Ergebnis warten soll).

    Der Aufruf sieht etwa so aus:

    VB.NET-Quellcode

    1. Sub Auftraggeber()
    2. Dim t As New Transaction(command, sendData, AddressOf Collector)
    3. Scheduler.Enqueue(t)
    4. End Sub
    5. Sub Collector(t As Transaction)
    6. t.SyncObject.WaitOne()
    7. DoWhatever(t.ReceiveBuffer)
    8. End Sub

    Man kann das Ganze auch nach einem bestimmten Muster implementieren, was du vielleicht als Begin*() und End*()-Methoden von anderen .NET-Objekten kennst. Dafür sollte man aber etwas Erfahrung mitbringen.
    Gruß
    hal2000
    Danke für die ausführliche Antwort.

    hal2000 schrieb:

    Synchronisationsobjekt: Du musst irgendwo auf Daten warten. Stell dir vor, du übergibst dem Scheduler 1000 Aufträge und möchtest das Ergebnis des 500sten haben. Das dauert aber, weil erstmal 499 andere Aufträge abgearbeitet werden müssen.

    Ich glaube, ich brauche das nicht. Das soll gar nicht asynchron laufen, wie ich jetzt verstanden habe: die nächste Transaktion soll erst durchgeführt werden, wenn die Antwort von der ersten vorliegt. Auch beim Pollen (da wird solange ein Bit im Register abgefragt, bis er 1 ist, dann kann ich Daten auslesen - währenddessen soll nichts anderes passieren).

    hal2000 schrieb:

    Besser ist es, eine Polling-Transaktion zu erstellen, die den Scheduler intern zum Pollen veranlasst, bis ein gültiger Wert zurückkommt. Das hat wiederum den Nachteil, dass der Scheduler währenddessen keine anderen Anfragen bearbeiten kann

    Ja, wie gesagt, in diesem Fall soll nichts anderes sein, und ich habe mir auch überlegt, dass ich eine Polling-Transaktion erstelle - Daten senden, warten, bis Bit=1 ist, dafür das Bit dauernd abfragen, Daten empfangen. Oder könnte man das auftrennen?

    hal2000 schrieb:

    Timer: Schlecht. Benutze einen separaten Thread und lass ihn entsprechend lange zwischendurch schlafen.

    Wie ich es verstanden habe, sollte bei Modbus alles in einem festen Zeitrahmen laufen. Bei mir gibt es nichts Zeitkritisches, d.h. Timer auf 100 ms ist vollkommen ausreichend, alle 100 ms eine Transaktion, da müsste das mit Polling auch hinkommen. Zur Not kann ich bei Polling den Timer auf 200 setzen und am Ende wieder auf 100, damit er nicht dazwischenfunkt.

    hal2000 schrieb:

    List(T): Eine Möglichkeit, wenn du Prioritäten verwalten musst. Auch diese musst du manuell synchronisieren.

    Ja, das mit Prioritäten habe ich mir auch überlegt: die kontinuierliche Lesevorgänge (es sind mehrere Registergruppen, am besten macht man je eine Transaktion daraus, oder?) sollen die niedrigste Priorität haben, damit usergesteuerte Vorgänge immer als Erstes erledigt werden, zumal der User ja gar nicht so häufig auf die Taste klickt, somit nicht so häufig Transaktion anstoßt.

    hal2000 schrieb:

    Das Callback wird nur einmal aufgerufen - entweder synchron (wenn das Ergebnis vorliegt) oder asynchron (wenn der Empfänger auf das Ergebnis warten soll).

    Das Letzte habe ich nicht so verstanden. Die Daten kommen doch im DataReceived-Event an, dann soll die Callback-Funktion aufgerufen werden - dann rufe ich die Funktion vom aktuellen Transaktionsobjekt an? Die List ist ja klassenweit gültig...

    hal2000 schrieb:

    was du vielleicht als Begin*() und End*()-Methoden von anderen .NET-Objekten kennst.

    Ne, kenne leider nicht, bin ja erst seit April im .NET unterwegs.

    Noch eine Frage:
    Ich füge Transaktionen der Liste von verschiedenen Klassen aus. Erstelle ich dann ein Schedulerobjekt in der Main und übergebe es den anderen Klassen, wo er gebraucht wird, per Konstruktor?
    Was nur einmal im Programm da sein darf, kannst Du
    einmal instanziieren und die Instanz per Property durchreichen
    oder
    Shared machen und so von überall aus zugreifbar machen.
    Ich mach letzteres.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    sonne75 schrieb:

    Ist es nicht das Gleiche?
    Das eine ist eine "gewöhnliche" Variable / Property, das andere ist eine "Shared" Variable / Property.
    Hier kannst Du sicher sein, dass sie nur in einer Ausführung (um nicht Instanz zu sagen) existiert.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    @sonne75:: Wie auch immer, das war gemeint.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Das soll gar nicht asynchron laufen
    Das macht die Sache einfacher. Dann brauchst du tatsächlich kein Synchronisationsobjekt in der Transaktion. Der Scheduler muss aber intern trotzdem synchronisiert werden, weil die Queue von einem anderen Thread abgearbeitet wird (sonst "hängt" die Form).

    Schreib bitte mal alle Befehle auf, die du an dein Gerät senden willst - auch die, die Polling erfordern. Daraus leiten wir dann die Klasse ab, die eine Transaktion darstellt.

    Zur Not kann ich bei Polling den Timer auf 200 setzen und am Ende wieder auf 100, damit er nicht dazwischenfunkt.
    Das ist genau der falsche Weg. Du kannst das Zeitverhalten eines Programms nicht manuell steuern, weil du nicht alle äußeren Umstände kennst. Was ist, wenn dein Rechner gerade ein Backup macht, dadurch die Festplatte stark belastet und dein Programm dann 201ms braucht? Dann stellst du den Timer zur Sicherheit auf 1s und die Kommunikation insgesamt bekommt eine Effizienz von 2% (= ausgeführte Vorgänge durch zeitlich insgesamt mögliche Vorgänge). Ich hoffe dir ist klar, was das für eine Bastellösung ist - verbanne das Wort "Timer" aus deinem Wortschatz.

    Das Letzte habe ich nicht so verstanden. Die Daten kommen doch im DataReceived-Event an, dann soll die Callback-Funktion aufgerufen werden - dann rufe ich die Funktion vom aktuellen Transaktionsobjekt an? Die List ist ja klassenweit gültig...
    Beim DataReceived-Event bist du noch gar nicht. Du arbeitest an den Interna des Schedulers, während du noch nicht mal weißt, welche Transaktionen es gibt. Das kommt später.

    Ne, kenne leider nicht, bin ja erst seit April im .NET unterwegs.
    Ist nicht weiter schlimm - dein Scheduler soll ja synchron laufen.

    Ich füge Transaktionen der Liste von verschiedenen Klassen aus. Erstelle ich dann ein Schedulerobjekt in der Main und übergebe es den anderen Klassen, wo er gebraucht wird, per Konstruktor?
    Der Scheduler ist eine eigene Klasse. Er verwaltet die Liste / PriorityQueue intern und gewährt niemandem sonst Zugriff darauf. Pro Gerät existiert eine global verfügbare Scheduler-Instanz. Bei der Instanzierung übergibst du konstante Geräteparameter wie Adresse, Timingverhalten und maximale Queue-Länge. Alles, was variabel ist, wird von den Transaktionsobjekten verwaltet, also Puffergrößen (sofern die mit den Befehlen variieren), Prioritäten und der auszuführende Befehl. Die Transaktionen übergibst du dem Scheduler direkt - der kümmert sich um das Einfügen in die Liste (und zwar je nach Priorität, die in der Transaktion vermerkt ist).

    So sieht der Scheduler etwa aus:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Class Scheduler
    2. Private q As (Priority-/Queue)(Of Transaction)
    3. Public Sub New(deviceAddress As String, queueLength As Int32)
    4. 'Parameter speichern, Queue erzeugen
    5. 'Thread erzeugen
    6. End Sub
    7. 'Ggf. Eigenschaften für weitere Initialisierung oder weitere Parameter im Konstruktor
    8. Public Sub Enqueue(t As Transaction)
    9. 't.Priority auswerten, entsprechend in die Liste einfügen
    10. 'Synchronisation nicht vergessen
    11. End Sub
    12. Public Sub Dispose()
    13. stopScheduler = True
    14. End Sub
    15. 'separater Thread
    16. Private Sub DoWork()
    17. Dim t As Transaction
    18. While Not stopScheduler
    19. t = q.GetFirst() 'Synchronisation!
    20. Thread.Sleep(100) 'Dem Gerät immer 100ms zwischen den Befehlen Ruhezeit geben
    21. Select Case t.Command
    22. Case X
    23. If t.PollingFlag Then
    24. 'Gerät.Polling(Befehl) 'Auch hier Ruhezeit einplanen (While-Schleife zum Pollen?)
    25. 't.ReceiveBuffer = Gerät.GetErgebnis()
    26. End If
    27. Case Y
    28. Case Else
    29. 'Gerät.Send(t.Befehl, t.SendBuffer)
    30. 'warte auf Ergebnis
    31. End Select
    32. t.Callback(t)
    33. End While
    34. 'Queue freigeben - was passiert mit noch vorhandenen Transaktionen?
    35. End Sub
    36. End Class


    Transaktion allgemein:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Class Transaction
    2. 'Private Variablen zum Speichern
    3. Public ReadOnly Property Command As DeviceCommands 'Enumeration
    4. Public ReadOnly Property SendBuffer As Byte()
    5. Public ReadOnly Property Callback As Action(Of Transaction)
    6. Public Property ReceiveBuffer As Byte()
    7. Public Sub New(cmd As DeviceCommands, bufferSize As Int32, callback As Action(Of Transaction))
    8. 'Puffer anlegen
    9. 'Parameter speichern
    10. End Sub
    11. End Class
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „hal2000“ ()

    hal2000 schrieb:

    Der Scheduler muss aber intern trotzdem synchronisiert werden, weil die Queue von einem anderen Thread abgearbeitet wird (sonst "hängt" die Form).

    Warum ein eigener Thread? Damit Usereingaben möglich sind? Mit "intern synchronisiert" kann ich noch nichts anfangen (inhaltlich), aber vielleicht kommt es dann von alleine.

    hal2000 schrieb:

    Schreib bitte mal alle Befehle auf, die du an dein Gerät senden willst - auch die, die Polling erfordern. Daraus leiten wir dann die Klasse ab, die eine Transaktion darstellt.

    Kontinuierliche Lesevorgänge (mehrmals pro Sekunde), jeweils pro existierenden Kanal, d.h. können nicht am Stück ausgelesen werden, 1-64 o. 1-32, die Kanalnummer bestimmt letztendlich die Registeradresse, ich habe eine Registerklasse pro Registerart (es gibt mehrere, je nach Aufbau der Daten), jedes Kanalobjekt hat bei mir seinen eigenen Registerobjekt mit Adresse usw:
    -Receiving Info Register
    -Receiving Register
    -Actuator Register

    Ich bekomme einmal Daten, die ich dann ins DataSet verteilen muss, und einmal mehrere Datenbytes, die ich mit Umrechenfunktionen bearbeite (sie sind schon fertig, es fehlt nur die Kommunikation), in einem Lesevorgang ist beides enthalten.

    Auf Befehl lesen
    -Inforegister

    Auf Befehl schreiben
    -Sender Register, aber nur eins immer, die Nummer ist bekannt
    -Actuator Register, genau so


    Mit dem Polling muss ich es noch genau klären, ich liefere es spätestens morgen nach.

    hal2000 schrieb:

    verbanne das Wort "Timer" aus deinem Wortschatz.

    Sagen wir so, einige Register müssen dauernd ausgelesen werden, allerdings kann man es auch so lösen, dass sie immer ausgelesen werden, wenn in der Queue sonst nichts mit höheren Prio drin ist. Dann braucht man den Timer nicht. D.h. die letzte Funktion startet zum Schluss die nächste Transaction (nach Prio), am Anfang vom Programm muss man das anstoßen. Meinst du das?

    hal2000 schrieb:

    Pro Gerät existiert eine global verfügbare Scheduler-Instanz. Bei der Instanzierung übergibst du konstante Geräteparameter wie Adresse, Timingverhalten und maximale Queue-Länge.

    Ich habe nur ein einziges Gerät, es ist per RS232 angeschlossen, die Geräteadresse für Modbus wird über Treiber geregelt. Beim Rest bin ich nicht sicher, ob ich das brauche...

    VB.NET-Quellcode

    1. t = q.GetFirst() 'Synchronisation!

    Was meinst du überhaupt damit? Die Queue gibt einfach mit .Dequeue das höchstpriorisierte Element...

    hal2000 schrieb:

    Public ReadOnly Property Command As DeviceCommands 'Enumeration

    Wie kann ich sie dann in New() beschreiben, wenn sie ReadOnly ist?

    Habe nachgegoogelt, sie sollte im Konstruktor setzbar sein, allerdings funktioniert es nicht. Habe noch eine Private angelegt, die bei Property zurückgegeben wird (habe VS 2010).

    Wie kommt jetzt DataReceived-Event ins Spiel? Gerade beim Pollen bekomme ich dauernd Daten zurück, die ich auswerten muss. Sie kommen alle im DataReceived-Event an: wie springe ich von da wieder zum Pollen im DoWork()?

    Oder beim Lesen? Ich schicke einen Lesebefehl ans Gerät und muss Daten empfangen - gut, das kann man über "Select Case state" verteilen, aber wo gehen die Daten aus dem Scheduler hin? Per Event an die zugehörigen Klassen? Dann müsste in Transaction jeweiliger Event gespeichert werden, oder?.

    Ich glaube, ich brauche doch gar kein DataReceived-Event. Ich habe die Modbus-DLL noch mal angeschaut, da gibt es eine ReadRegister-Funktion, die sofort schon die Daten zurückgibt. Am Anfang wird ein SerialPort als Master konfiguriert (in einem der beiden angegebenen Modi) und anscheinend wird alles intern geregelt. :thumbup:

    Alles, was wir bisher gemacht haben (mit Queue und Transaction) bleibt trotzdem in Kraft, ich bin jetzt gerade an der Transaction-Klasse dran, da kommen noch einige Propertys hinzu..

    Eine Frage habe ich noch, bei der Callback-Funktion, kann sie Funktion aus beliebiger Klasse sein (Public natürlich)? Dann würde ich sie gleich für die Datenverteilung aus dem Receivebuffer nutzen...

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „sonne75“ ()

    Hi, sorry für die späte Antwort - hatte am Wochenende nur wenig Zeit.

    sonne75 schrieb:

    Warum ein eigener Thread? Damit Usereingaben möglich sind?
    Genau. Wenn du alles im Hauptthread erledigst, kann deine Anwendung keine Benutzereingaben mehr verarbeiten (Klicks, Tastendrücke, etc.). Deswegen muss der Scheduler in einem eigenen Thread laufen.

    sonne75 schrieb:

    Mit "intern synchronisiert" kann ich noch nichts anfangen (inhaltlich)
    Das bedeutet, dass die Methoden des Schedulers threadsicher auf die interne Queue zugreifen. Ist der Zugriff nicht synchronisiert, entsteht Chaos in der Queue, wenn der Scheduler ein Element entfernt und gleichzeitig eine Transaktion vom Benutzer eingefügt wird.

    sonne75 schrieb:

    Sagen wir so, einige Register müssen dauernd ausgelesen werden, allerdings kann man es auch so lösen, dass sie immer ausgelesen werden, wenn in der Queue sonst nichts mit höheren Prio drin ist. Dann braucht man den Timer nicht. D.h. die letzte Funktion startet zum Schluss die nächste Transaction (nach Prio), am Anfang vom Programm muss man das anstoßen. Meinst du das?
    Siehe unten.

    sonne75 schrieb:

    Was meinst du überhaupt damit? Die Queue gibt einfach mit .Dequeue das höchstpriorisierte Element...
    Ja, GetFirst() ist nur ein Synonym für Dequeue().

    sonne75 schrieb:

    Habe nachgegoogelt, sie sollte im Konstruktor setzbar sein, allerdings funktioniert es nicht. Habe noch eine Private angelegt, die bei Property zurückgegeben wird
    Du setzt nicht die Property, sondern einfach die private Variable im Konstruktor:

    VB.NET-Quellcode

    1. Class Beispiel
    2. Private _var As Int32
    3. Public ReadOnly Property Var As Int32
    4. Get
    5. Return _var
    6. End
    7. End Property
    8. Public Sub New(var As Int32)
    9. _var = var
    10. End Sub
    11. End Class


    sonne75 schrieb:

    ReadRegister-Funktion
    Das ist schonmal gut - das können wir später gebrauchen.

    sonne75 schrieb:

    Eine Frage habe ich noch, bei der Callback-Funktion, kann sie Funktion aus beliebiger Klasse sein (Public natürlich)? Dann würde ich sie gleich für die Datenverteilung aus dem Receivebuffer nutzen...
    Das kannst du tun - sie muss nicht mal Public sein, weil du ja eine Referenz darauf übergibst, die der Scheduler auch aufrufen kann, wenn die Zielfunktion Private ist.


    Wie oben angekündigt hier noch was zum Polling: Gute Idee - der Scheduler kann ja arbeiten, ohne dass in die Queue ständig Polling-Transaktionen vorhanden sein müssen. Das würde ich so lösen:
    - Der Scheduler verwaltet neben der Queue eine Liste mit Polling-Transaktionen.
    - Es gibt Methoden, die die Polling-Liste bearbeiten können:

    VB.NET-Quellcode

    1. Private pollingTransactions As List(Of Transaction)
    2. Public Sub AddContinuousTransaction(t As Transaction)
    3. pollingTransactions.Add(t)
    4. End Sub
    5. 'ggf. weitere Methoden, z.B. Entfernen von Polling-Transaktionen


    - Die Schedule-Schleife sieht so aus:
    -- Wenn Queue nicht leer --> nimm Queue-Transaktion, führe sie aus, Loop.
    -- Wenn Queue leer --> Polling-Liste abarbeiten, Loop.

    Damit kannst du dir sogar die Prioritäten sparen, denn die Transaktionen in der "normalen" Queue sind durch die Abfrage implizit höher priorisiert als das Polling.

    Mit deinen aufgeführten Befehlen sieht deine Transaktion so aus:

    VB.NET-Quellcode

    1. Class Transaction
    2. 'Private Variablen zum Speichern
    3. Public ReadOnly Property Command As DeviceCommands
    4. Public ReadOnly Property RegisterNumber As Int32
    5. Public ReadOnly Property Callback As Action(Of Transaction)
    6. Public ReadOnly Property SendBuffer() As Byte
    7. Public Property ReceiveBuffer As Byte()
    8. Public Sub New(cmd As DeviceCommands, regNum As Int32, callback As Action(Of Transaction))
    9. 'Puffer anlegen, wenn erforderlch
    10. 'Parameter speichern
    11. 'ggf. SendBuffer aus Command und RegisterNumber intern zusammenbauen - das macht die Transaktion intelligent.
    12. End Sub
    13. End Class
    14. Public Enum DeviceCommands
    15. Read
    16. Write
    17. Poll
    18. End Enum


    Scheduler (stark vereinfacht):

    VB.NET-Quellcode

    1. Class Scheduler
    2. Private pollingTransactions As List(Of Transaction)
    3. Public Sub AddContinuousTransaction(t As Transaction)
    4. pollingTransactions.Add(t)
    5. End Sub
    6. Private Sub DoWork()
    7. While True
    8. If q.Count > 0 Then
    9. 't = Dequeue, Ausführen (z.B. If t.Command = Read, dann t.ReceiveBuffer = ReadRegister(t.RegisterNumber)), t.Callback()
    10. Else
    11. For Each t In pollingTransactions
    12. 't ausführen, Callback
    13. Next
    14. End If
    15. End While
    16. End Sub
    17. End Class


    Und aufrufen kannst du das Ganze so:

    VB.NET-Quellcode

    1. Class Form1
    2. Private s As New Scheduler(...)
    3. Private Sub Button_Click()
    4. Dim t1 As New Transaction(DeviceCommands.Poll, 5, AddressOf MyPollingCallback)
    5. s.AddContinuousTransaction(t1)
    6. s.Start() 'Oder ähnlich - der Scheduler kann z.B. auch in seinem Konstruktor automatisch loslaufen.
    7. Dim t2 As New Transaction(DeviceCommands.Write, 2, AddressOf MyOtherCallback)
    8. s.AddSingleTransaction(t2)
    9. End Sub
    10. Private Sub MyPollingCallback(t As Transaction)
    11. 'Hier kommt das Ergebnis von jedem Polling-Vorgang
    12. End Sub
    13. Private Sub MyOtherCallback(t As Transaction)
    14. 'Hier kommt das Ergebnis des Schreibvorgangs
    15. End Sub
    16. End Class
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „hal2000“ ()

    hal2000 schrieb:

    Das bedeutet, dass die Methoden des Schedulers threadsicher auf die interne Queue zugreifen. Ist der Zugriff nicht synchronisiert, entsteht Chaos in der Queue, wenn der Scheduler ein Element entfernt und gleichzeitig eine Transaktion vom Benutzer eingefügt wird.

    Das sehe ich ein, allerdings weiß ich nicht, wie man das macht.

    hal2000 schrieb:

    Du setzt nicht die Property, sondern einfach die private Variable im Konstruktor:

    Ja, so habe ich es auch dann gemacht.

    hal2000 schrieb:

    Das ist schonmal gut - das können wir später gebrauchen. Schau bitte in der Dokumentation nach, ob diese Funktion threadsicher ist.

    Es gibt leider keine Dokumentation dazu, im Objektkatolog sieht man nur Signaturen.

    hal2000 schrieb:

    Wie oben angekündigt hier noch was zum Polling: Gute Idee - der Scheduler kann ja arbeiten, ohne dass in die Queue ständig Polling-Transaktionen vorhanden sein müssen. Das würde ich so lösen:
    - Der Scheduler verwaltet neben der Queue eine Liste mit Polling-Transaktionen.
    - Es gibt Methoden, die die Polling-Liste bearbeiten können:


    Am Freitag habe ich auch geschnallt, dass Polling auf zwei Arten verstanden werden kann:
    Du meinst jetzt das zyklische Lesen. Das ist gar nicht so schlecht, zumal ich erfahren habe, dass das zyklische Lesen durch User an- und abstellbar sein soll.

    Ich meinte den "Anlernvorgang" von einem Drittgerät: da wird erst was geschrieben, dann ein Bit solange gelesen, bis er 1 ist, und dann Daten gelesen. Das ist eine besondere Art des Transaction-Commando, die aus mehreren Teilen besteht.

    hal2000 schrieb:

    - Die Schedule-Schleife sieht so aus:
    -- Wenn Queue nicht leer --> nimm Queue-Transaktion, führe sie aus, Loop.
    -- Wenn Queue leer --> Polling-Liste abarbeiten, Loop.

    Damit kannst du dir sogar die Prioritäten sparen, denn die Transaktionen in der "normalen" Queue sind durch die Abfrage implizit höher priorisiert als das Polling.


    Ja, da hast du Recht.

    Ich habe jetzt das mit Action (Of T) hingekriegt, ich hatte Verständnisprobleme. Zudem habe ich rausgefunden, dass es nur eine Lesefunktion aus der DLL gibt, und 2 Sendefunktionen (1 Registerzelle auf einmal und mehrere).

    So sieht bisher der Aufruf aus dem ReceiveRegister (es gibt nur diese zwei Transactionen pro Kanal):

    VB.NET-Quellcode

    1. Public Sub Read()
    2. Dim Data(1) As UShort
    3. Dim length As UShort
    4. Dim nmb = _rwChan.SensorRow.ProfileRow.NumberOfDB
    5. Select Case nmb
    6. Case 1, 4
    7. length = nmb
    8. Case Else
    9. length = CUShort(nmb \ 2 + 1)
    10. End Select
    11. length = CUShort(length + RECEIVE_REG.DB_0_01)
    12. Dim t1 = New Transaction(MB_COMMAND.READ_REGISTERS, 0, start_rec_reg + reg_length * _
    13. CUShort(_rwChan.ChanID), length, Data, AddressOf RecRegResult)
    14. _ModbusScheduler.Add(t1)
    15. Dim t2 = New Transaction(MB_COMMAND.READ_REGISTERS, 0, start_rec_info_reg + receive_info_reg_length * _
    16. CUShort(_rwChan.ChanID), receive_info_reg_length, Data, AddressOf InfoRecRegResult)
    17. _ModbusScheduler.Add(t2)
    18. End Sub
    19. Public Sub RecRegResult(ByVal t As Transaction)
    20. End Sub
    21. Public Sub InfoRecRegResult(ByVal t As Transaction)
    22. End Sub


    Am Anfang muss ich Länge ausrechnene, aber es kann sein, dass das auch wegfällt.

    Hier ist die Transaction-Klasse (die Propertys habe jetzt hier weggelassen):

    VB.NET-Quellcode

    1. Public Sub New(ByVal cmd As MB_COMMAND, ByVal prio As Integer, ByVal startaddr As UShort, _
    2. ByVal nmbOfPoints As UShort, ByVal senddata As UShort(), ByVal callback As Action(Of Transaction))
    3. _Command = cmd
    4. _priority = prio
    5. _startaddress = startaddr
    6. _nmbOfPoints = nmbOfPoints
    7. ' _callback = Callback
    8. Dim length = Min(_SendBuffer.Length, senddata.Length)
    9. For i = 0 To length - 1
    10. _SendBuffer(i) = senddata(i)
    11. Next
    12. End Sub
    13. End Class
    14. Public Enum MB_COMMAND
    15. READ_REGISTERS
    16. SEND_REGISTER
    17. SEND_MULT_REGISTER
    18. LEARN
    19. End Enum


    Und hier ist der Anfang vom Scheduler:

    VB.NET-Quellcode

    1. Public Class ModbusScheduler
    2. Private Queue As New PriorityQueue(Of Transaction)
    3. Public Property slaveID As Byte
    4. Private mbSerialMaster As Modbus.Device.ModbusSerialMaster
    5. Public Sub New(ByVal IsRTU As Boolean, ByVal mbAddr As Byte, ByVal serPort As SerialPort)
    6. If IsRTU Then
    7. mbSerialMaster = ModbusSerialMaster.CreateRtu(serPort)
    8. Else
    9. mbSerialMaster = ModbusSerialMaster.CreateAscii(serPort)
    10. End If
    11. slaveID = mbAddr
    12. End Sub
    13. Public Sub Add(ByVal transaction As Transaction)
    14. Queue.Enqueue(transaction, transaction.priority)
    15. End Sub
    16. Public Sub NextAction()
    17. Dim tr As Transaction = Nothing
    18. Queue.Dequeue(tr)
    19. If tr Is Nothing Then Exit Sub
    20. Select Case tr.Command
    21. Case MB_COMMAND.READ_REGISTERS
    22. tr.RecBuffer = mbSerialMaster.ReadHoldingRegisters(slaveID, tr.startaddress, tr.nmb)
    23. Case MB_COMMAND.SEND_REGISTER
    24. Case MB_COMMAND.LEARN
    25. End Select
    26. 'Wait
    27. NextAction()
    28. End Sub
    29. End Class


    Den muss ich noch auf zwei Queues umbauen. "Wait" - da muss ich eine bestimmte Zeit warten, das werde ich später einbauen.
    Ich hab meinen alten Beitrag noch ein paarmal editiert. Zum Beispiel habe ich die Sache mit der Threadsicherheit von ReadRegister entfernt. Das ist übrigens ein tolles Live-Beispiel, was auch in der Queue passiert, wenn sie nicht synchronisiert ist (so wie wir gerade bzw. unser gleichzeitiger, unsynchronisierter Zugriff auf dieses Thema).

    sonne75 schrieb:

    Das sehe ich ein, allerdings weiß ich nicht, wie man das macht.
    Das geht so:

    VB.NET-Quellcode

    1. Class Scheduler
    2. Private oSync As New Object
    3. Public Sub Add(...)
    4. SyncLock oLock
    5. q.Enqueue(t)
    6. End SyncLock
    7. End Sub
    8. Private Sub DoWork()
    9. Dim t As Transaction
    10. SyncLock oLock
    11. t = Dequeue() 'Wenn nicht leer (der Test mit If muss auch ins SyncLock)
    12. End SyncLock
    13. End Sub


    sonne75 schrieb:

    Es gibt leider keine Dokumentation dazu, im Objektkatolog sieht man nur Signaturen.
    Ist auch nicht mehr relevant, weil wir nur noch einen weiteren Thread benutzen. Mit einem zusätzlichen Timer wäre es schwieriger geworden.

    sonne75 schrieb:

    NextAction()
    Ganz böse - das fliegt dir nach kurzer Zeit mit einer StackOverflowException um die Ohren. Du rufst dich immer weiter selbst rekursiv auf bis der Speicher irgendwann voll ist. Jetzt kommt nämlich der separate Thread ins Spiel:

    VB.NET-Quellcode

    1. Class Scheduler
    2. Private worker As System.Threading.Thread
    3. Public Sub New(...)
    4. '...
    5. worker = New Thread(AddressOf DoWork)
    6. worker.Start()
    7. End Sub
    8. Private Sub DoWork()
    9. While True 'Spezielle Stop-Transaktion + Exit While oder separate Methode, die eine Boolean-Variable ändert (While Not bStop)
    10. t = Dequeue() 'SyncLock
    11. Select Case t.Command
    12. '...
    13. End Select
    14. End While
    15. End Sub
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „hal2000“ ()

    hal2000 schrieb:

    Zum Beispiel habe ich die Sache mit der Threadsicherheit von ReadRegister entfernt. Das ist übrigens ein tolles Live-Beispiel, was auch in der Queue passiert, wenn sie nicht synchronisiert ist (so wie wir gerade bzw. unser gleichzeitiger, unsynchronisierter Zugriff auf dieses Thema).

    Das verstehe ich gerade nicht...

    Wie wird es eigentlich mit der Polling-Queue gehandhabt? Werden da Elemente auch entfernt? Ich werde da zig Transactions haben (pro Kanal 2 zum Lesen, es sind 1-64 Kanäle), wer fügt sie dann wieder rein? Was wäre es dann der Unterschied zu einer Priority Queue? So oder so müssen sie immer wieder eingefügt werden. Oder macht man das nach dem .Dequeue(t), sofort .Enqueue(t) wieder dran?

    Aber dann kann ich List(Of) ja eher weniger gebrauchen, oder? Da wird ja nichts in der Reihenfolge gegeben, meinst du dann "Queue(Of)"?
    Das verstehe ich gerade nicht...
    Ich habe meinen Beitrag geschrieben (mit dem Begriff Threadsicherheit darin). Den hast du gelesen. Gleichzeitig habe ich den Begriff in einem Editiervorgang aber entfernt, was du aber nicht bemerkt hast (denn du bist auf den bereits gelöschten Begriff noch eingegangen). In Wirklichkeit war der Begriff aber gar nicht mehr vorhanden. Das ist der wunde Punkt: Thread A ändert irgendwas an der Queue, Thread B bekommt das nicht mit und versucht, mit dem alten, ungültigen Zustand weiterzuarbeiten --> Crash. Wir beide können dann noch miteinander reden, aber das Programm stürzt einfach ab.

    sonne75 schrieb:

    Wie wird es eigentlich mit der Polling-Queue gehandhabt? Werden da Elemente auch entfernt? Ich werde da zig Transactions haben (pro Kanal 2 zum Lesen, es sind 1-64 Kanäle), wer fügt sie dann wieder rein?
    Nein, ich meine tatsächlich List(Of T). Da wird nichts entfernt. Die Polling-Transaktionen bleiben in der List(Of T) drin, bis sie von außen (vom Aufrufer des Schedulers) manuell entfernt werden. Die Liste wird immer dann abgearbeitet, wenn die Queue gerade leer ist. Nur aus der Queue werden (die Einmal-Transaktionen) automatisch entfernt.

    Den Anlern-Vorgang für das Gerät würde ich aufteilen. So wie du das beschrieben hast, wird erst ein Register geschrieben, dann wird gepollt, also nachgesehen, ob das Gerät die Änderung mitbekommen hat, und dann wird ggf. noch was anderes gemacht. Das würde ich so realisieren:
    - Einmal-Transaktion "Write"
    - Im Callback der Write-Transaktion eine Polling-Transaktion für dasselbe Register einfügen
    - Das Polling-Callback sagt irgendwann, dass das Gerät wieder bereit ist
    - Dort dann die Polling-Transaktion entfernen und die nächste Transaktion starten

    --> Das geht immer zwischen den Callbacks hin- und her, bis ein großer Gesamtvorgang abgeschlossen ist.
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „hal2000“ ()

    hal2000 schrieb:

    Wir beide können dann noch miteinander reden, aber das Programm stürzt einfach ab.

    Jetzt habe ich es. :D Du hast unsere Kommunikation als Beispiel gebracht :)

    Das SyncLock kenne ich noch von LabView, ich dachte mir schon, dass es so gehen muss...

    hal2000 schrieb:

    Nein, ich meine tatsächlich List(Of T). Da wird nichts entfernt.

    hal2000 schrieb:

    bis sie von außen (vom Aufrufer des Schedulers) manuell entfernt werden.

    Aber dann müsste ich die zig Transaktionen alle als Objekt speichern, um sie dann manuell entfernen zu können? Wie ich verstanden habe, soll entweder zyklisch gelesen werden (für alles) oder gar nicht.

    Warum nicht so:

    sonne75 schrieb:

    Oder macht man das nach dem .Dequeue(t), sofort .Enqueue(t) wieder dran?

    Wenn Pollingflag auf False gesetzt wird, dann prüft man halt die PollingQueue nicht mehr.

    hal2000 schrieb:

    --> Das geht immer zwischen den Callbacks hin- und her, bis ein großer Gesamtvorgang abgeschlossen ist.

    Wozu das? In der Zwischenzeit soll sowieso nichts passieren, bis das Gerät das Drittgerät angemeldet hat. Es ist eine festvorgegebene Reihenfolge, mit While (Bit=0) dazwischen... Was würde mir die Aufteilung für Vorteile bringen?


    Ich danke dir sehr, ich bin in den letzten 2 Arbeitstagen schon sehr weit gekommen. :love:

    sonne75 schrieb:

    Warum nicht so:
    Weil die Scheduling-Loop streng genommen kein Enqueue() ausführen darf - der Scheduler würde sich ja dann selbst mit Arbeit zuschütten. Das halte ich für nicht sinnvoll.

    sonne75 schrieb:

    Aber dann müsste ich die zig Transaktionen alle als Objekt speichern, um sie dann manuell entfernen zu können?
    Nein, du musst die Transaktionen nicht speichern. Im Callback kommt doch die aktuelle Transaktion gleich als Parameter mit - nur eben um ein Ergebnis angereichert. Polling-Transaktionen müsstest du noch um eine Art ID erweitern, damit du sie in der Liste eindeutig identifizieren kannst (durch das Hinzufügen des Ergebnisses ändert sich das Objekt). Anhand dieser ID kannst du die Transaktion dann entfernen.

    sonne75 schrieb:

    Wie ich verstanden habe, soll entweder zyklisch gelesen werden (für alles) oder gar nicht.
    Klar- Es werden alle Transaktionen, die für Polling registriert sind, auch gepollt.

    sonne75 schrieb:

    Wozu das? In der Zwischenzeit soll sowieso nichts passieren, bis das Gerät das Drittgerät angemeldet hat. Es ist eine festvorgegebene Reihenfolge, mit While (Bit=0) dazwischen... Was würde mir die Aufteilung für Vorteile bringen?
    Bisher meintest du, dass auch während des Pollings einzelne Benutzereingaben möglich sein müssen. Deswegen hattest du ja zuerst die PriorityQueue eingeführt (die du jetzt übrigens durch eine normale Queue(T) ersetzen könntest). Oder gibt es zwei Arten von Polling, einmal beim Anlernen (ohne Unterbrechungsmöglichkeit) und einmal "allgemein" (mit Unterbrechungsmöglichkeit)?

    Wenn es ausschließlich die erste Variante gibt, brauchst du die ganze Liste nicht, weil dann ein Befehl eben mit seinem zwischenzeitlichen Polling den Scheduler blockiert. Die Befehle werden dann einfach im Select Case des Schedulers seriell codiert, wie du schon geschrieben hast. Ob du die Liste brauchst oder nicht, hängt davon ab, ob auch längere Befehle (Befehlsgruppen, die Polling enthalten) unterbrechbar sein sollen oder ob diese atomare Operationen darstellen sollen.

    Angenommen das Gerät braucht 10 Sekunden für einen "Anlernvorgang" (was immer das bei dir ist). 1s für den Write-Befehl, 8s bis das Gerät die Änderung mitbekommt und 1s für einen weiteren Befehl. Wenn der Benutzer nun einen anderen Befehl an den Scheduler absetzt, muss er im schlechtesten Fall 10s auf die Ausführung warten. Ist das Polling unterbrechbar (weil es ja keine so hohe Priorität haben soll), muss er maximal 1s warten (bis der Read-befehl abgeschlossen ist). Wenn der Anlernvorgang aber insgesamt nur 500ms dauert, kannst du dir die ganzen Überlegungen sparen, weil eine Wartezeit in dieser Größenordnung für einen Benutzer weniger relevant ist.
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „hal2000“ ()

    hal2000 schrieb:

    Anhand dieser ID kannst du die Transaktion dann entfernen.

    Es geht ja darum, dass der Benutzer einstellt: soll zyklisch gelesen werden oder nicht. D.h. eigentlich müsste ich gar nichts aus dieser Queue entfernen, sondern nur Flag prüfen, ob zyklisch gelesen werden soll.

    hal2000 schrieb:

    Oder gibt es zwei Arten von Polling, einmal beim Anlernen (ohne Unterbrechungsmöglichkeit) und einmal "allgemein" (mit Unterbrechungsmöglichkeit)?


    Ja, das meinte ich, es ist eine Bezeichnung für unterschiedliche Vorgänge. Einmal das Anlernen (Zuweisung vom Drittgerät an ein Kanal vom Gerät, sie handeln das untereinander aus, bis ich dann die Daten abholen darf, ich kündige es dem Gerät vorher an, dass an dem Gerät gleich was ankommt) und einmal eben das zyklische Lesen, was du als Polling bezeichnest.

    Der Anlernvorgang soll am Stück laufen, da ist es, denke ich, nicht wichtig, dass keine Daten kommen (werde aber noch abklären). Der User muss den Vorgang beim Drittgerät manuell durch Knopf drücken auslösen, der User ist also sowieso beschäftigt und gibt nichts ab. Ich muss nur klären, ob zyklisches Lesen währenddessen wichtig ist.

    Das zyklische Lesen hat sehr viele Transaktionen (wenn z.B. 30 Kanäle belegt sind, dann 60), deswegen wollte ich jeden anderen Befehl dazwischen schieben können -> Priority oder eben diese Polling-Queue.