Laden...

GUI-Applikation & verschiedene Hardware-Schnittstellen: 3-Schichtenmodell richtig verstanden?

Erstellt von Locutus vor 3 Jahren Letzter Beitrag vor 3 Jahren 601 Views
Locutus Themenstarter:in
68 Beiträge seit 2008
vor 3 Jahren
GUI-Applikation & verschiedene Hardware-Schnittstellen: 3-Schichtenmodell richtig verstanden?

Hallo,

ich hatte letzthin das Vergnügen, dass ich für einen Kollegen eine Applikation zur Steuerung einer Microcontrollerschaltung schreiben durfte. Die tut auch soweit und keine Fehler bisher. Entstanden ist die Applikation allerdings kurzfristig auf Zuruf und "überraschend", weil nicht eingeplant, aber natürlich dringend 😠

Meine C#/.NET-Kenntnisse sind relativ begrenzt, ich verwende es hauptsächlich für eigene kleine Testprogramme. Das Programm für den Kollegen war komplexer als das was ich bisher geschrieben habe, dementsprechend war da überhaupt keine gescheite Trennung zwischen Zugriff auf die Hardwareschnittstelle, Programmlogik und GUI drin. Nun steht die nächste Schaltung an, und diesmal konnte ich wenigstens vorher abklären, dass auch hierfür wieder eine Applikation her muss. Nur würd ich's diesmal gern "richtiger" machen und nicht mit der heißen Nadel stricken - außerdem kann ich dabei gleich was lernen.

Den Artikel zum Drei-Schichten-Modell hab ich durchgelesen, der Ansatz scheint genau das passende zu sein, wenn ich die Datenzugriffsschicht durch eine Kommunikationsschicht ersetze. Der Artikel beschreibt m.E. was zu tun ist, ich möchte nun das wie herausfinden.

Die Applikation soll hauptsächlich Daten von Sensoren visualisieren und die Möglichkeit bieten, die Einstellungen der Sensoren zu modifizieren. Für die Visualisierung selbst werde ich vermutlich OxyPlot verwenden. Genügend Beispiele sind soweit ich sehen kann vorhanden, das sollte also kein Problem sein.
Als Schnittstelle zur Hardware kommen aktuell entweder "echte" COM-Ports oder USB-UART-Wandler zum Einsatz.
Die Logikschicht dazwischen wird vermutlich außer dem Parsen der als Strings ankommenden Daten und dem Konfigurieren der Sensoren nicht viel zu tun haben.

Die Firmware für die Sensorhardware wird ebenfalls selbst geschrieben, d.h. Protokoll ist ziemlich frei. An der Ecke hab ich wesentlich mehr Erfahrung 🙂

Okay, nun zu meiner geplanten Umsetzung des 3-Schichten-Modells, ich fange bei der untersten, der Kommunikationsschicht an:
Für die Kommunikation zur Logikschicht würde ich ein Interface mit den Funktionen Read() und Write() sowie einem Event für angekommene Daten definieren. Für die Kommunikation selbst würde ich zwei unabhängige Klassen für die "echten" COM-Ports und die USB-UART-Wandler implementieren. Hintergrund dafür ist, dass ich bei den USB-UART-Wandlern über die Vendor- und Product-ID die Sensorhardware eindeutig erkennen kann. Bei den echten COM-Ports muss ich jeden Port durchgehen und per Kommando/Antwort prüfen, ob die Sensorhardware dran hängt. In beiden Fällen handelt es sich um eine automatische Erkennung. Falls bei weiteren Projekten ganz auf COM-Ports bzw. USB-UART-Wandler verzichtet und stattdessen bspw. direkt über USB-Endpunkte kommuniziert wird, will ich dafür gerüstet sein 🙂 Daher das magere Interface.
Hier stellen sich zwei Fragen:

  • ist diese Form der Erkennung in der untersten Schicht korrekt oder gehört sowas schon zur Logik- oder einer weiteren Zwischenschicht?
  • da es sich in den beiden genannten Fällen aus Sicht des Betriebssystems um serielle Ports handelt, könnte ich auch nur eine Klasse definieren, in welcher geprüft wird, ob eine Hardware mit passender VID/PID angeschlossen ist und zu dieser den entsprechenden Port ermitteln. Ist dies nicht der Fall, werden die COM-Ports manuell geprüft. Hätte zumindest den Vorteil, dass ich (theoretisch) nur einmal die Handhabung des seriellen Ports implementieren muss. Gibt's da sowas wie den "richtigen" Ansatz oder ist das eher Geschmackssache?

Die Logikschicht implementiert die Kommandos zur Hardware und übergibt sie nach "unten" über das genannte Interface mittels Write(). Antwortet die Hardware, wird das der Logikschicht mittels einem Event mitgeteilt und die Antwort per Read() ausgelesen und aufgearbeitet. Die Logikschicht hält die Sensordaten vor und wenn diese aktualisiert wurden, wird ein Event ausgelöst, über welches die GUI die Daten zur Anzeige entgegennehmen kann.

Von der GUI zur Logikschicht, also bspw. für die Einstellungen der Sensoren, hier bin ich unschlüssig, wie das aussehen soll. Zum einen habe ich mindestens zwei Typen von Sensoren und diese ggf. in unterschiedlicher Anzahl sowie dementsprechend unterschiedlichen Einstellungen, und zum anderen weiß ich nicht, wie man das nach "oben" bekannt machen kann. Ist ein bisschen schwierig zu beschreiben, wo es da klemmt.
Ich frag mal so: Davon ausgehend, dass die Programmlogik von der GUI getrennt ist, wie teilt beispielsweise die Programmlogik einem GUI-Listenfeld für Einstellungen mit, welche Einstellungen vorhanden/erlaubt sind? Ich nehm mal das Beispiel der SerialPort-Klasse, dort gibt es eine Methode "GetPortNames()", welche eine Liste mit den vorhandenen COM-Ports zurückliefert. Das Listenfeld wird mit der Liste gefüllt, und das gewählte Listenelement umgekehrt als zu öffnender Port an die Logikschicht übergeben. Im Prinzip müsste ich ja dann genau sowas für alle Einstellungen in der Logikschicht implementieren, oder? Im Fall von numerischen Werten prüft die Logikschicht, ob der Wert innerhalb der erlaubten Grenzen ist, wenn nicht wird eine Ausnahme abgefeuert, welche von der GUI abgefangen und bspw. in eine Infobox ausgegeben wird.

Bzgl. der verschiedenen Sensortypen, kann man die in einer einzelnen Logikschicht implementieren oder nimmt man zwei parallele Schichten? Letzteres macht m.E. keinen Sinn, weil beide dann auf eine Hardware-Schnittstelle zugreifen. Wenn die Sensoren aber in einer Schicht implementiert werden, wie sieht es dann bspw. mit Erweiterungen um andere Sensortypen aus? Ein gemeinsames Interface kann es ja dann nicht geben, weil die Sensoren ja unterschiedliche Eigenschaften/Funktionen haben. Wie wird sowas gelöst?

Ist das o.g. der richtige Ansatz oder zumindest die richtige Richtung, um das 3-Schichtenmodell korrekt zu implementieren? Oder noch zu unsauber bzw. vielleicht sogar das falsche Implementierungsmodell?

Grüße

====================================
1001011010101010101101110101111000101010101010
Ich assimilier dich...
Und dich auch...
Ich mein's ernst!

1.378 Beiträge seit 2006
vor 3 Jahren

Hallo Locutus,

das ist schon mal ein guter Ansatz den du da vorgeschlagen hast und einiges bei der Implementierung ist mMn. wirklich Geschmackssache und solche Vorgehensmodelle sollen dabei helfen die Komplexität zu reduzieren sowie Test- und Wartbarkeit verbessern.

Ich würde auch so Anfangen die externen Resourcen zu kapseln. Die verschiedenen Hardware-Schnittstellen zB. durch ein gemeinsames Interface kann für gewisse Consumer im Code Sinn machen um einheitlich mit den Schnittstellen zu kommunizieren aber speziell die Konfiguration der unterschiedlichen Hardware-Schnittstellen wird sich mit einem allgemeinem Interace nicht abbilden lassen und somit wird man vermutlich Richtung GUI konkretere Interfaces pro Hardware-Schnittstelle brauchen.

Mal abgesehen von den Interfaces die man dafür definiert, der Zugriff sollte ausgelagert und gekapselt sein, was das Testen dieser Schnittstellen erst ermöglicht.

Eine Logik-Klasse kann wahlweise entweder alle Schnittstellen kennen und bedienen oder noch einmal separat pro Schnittstelle existieren - und vlt. noch eine Logik-Klasse darüber haben die diese wieder vereint... Auch wieder aus dem selben Aspekt der Test-und Wartbarkeit. Wenn die Logik sich pro Hardware-Schnittstelle gravierend unterscheidet würd ich teilen, ansonsten reichts wsl. in einer.

Die Logic-Klasse kann wie du vorgeschlagen hast per Calls und Events mit den Hardware-Schnittstellen-Klassen kommunizieren und selbst seine Zugriffe auch wieder durch Funktionen und Events öffentlich machen.

Richtung GUI (angenommen du verwendest WPF) hast du dann ein ViewModel pro View die du darstellen willst, welche die notwendigen Properties (für die View) im Sinne von MVVM an die Gui-Elemente bindet. Die ViewModels sind dann für die Interaktion mit den Logics verantwortlich.

Dein Beispiel mit GetPorts(): Das ViewModel ruft zB. bei der Initialisierung (oder auf Abruf per ICommand von der View getriggert) die Funktion der Logic.GetPorts() auf und setzt das Result einem "Port[] Ports{get;set;}" Property, welches durch INotifyPropertyChanged die GUI benachrichtig, dass diese sich aktualisieren und neue Werte darstellen soll.
Das ViewModel hat quasi die Aufgabe, die Logics für die View besser konsumierbar zu machen.

Auch in den Views und ViewModels: Wenn du die Hardware-Schnittstellen per View Konfigurieren willst, brauchst du wsl. spezifische Views pro Hardware-Schnittstelle (da wsl. unterschiedliche Parameter konfiguriert werden) und dazu spezifische ViewModels, welche diese dann den Logics order Hardware-Schnittstellen-Klassen weiterleiten können.

Also zusammengefasst schaut dein Ansatz unterhalb der GUI schon ordentlich aus und Richtung GUI schau dir [Artikel] MVVM und DataBinding an und auch den WPF Example Code von OxyPlot an.

Da du durch das Auslagern des Codes in verschiedene Komponenten wsl. viele Klassen inkl. Interfaces erzeugen wirst empfiehlt sich Dependency Injection einzusetzen um die Abhängigkeiten zu reduzieren, das Kompositionieren der Services zu vereinfachen und vor allem um den Code testbarer zu machen.

LG, XXX

Locutus Themenstarter:in
68 Beiträge seit 2008
vor 3 Jahren

Hallo XXX,

vielen Dank für deine Antwort.

Ich würde auch so Anfangen die externen Resourcen zu kapseln. Die verschiedenen Hardware-Schnittstellen zB. durch ein gemeinsames Interface kann für gewisse Consumer im Code Sinn machen um einheitlich mit den Schnittstellen zu kommunizieren aber speziell die Konfiguration der unterschiedlichen Hardware-Schnittstellen wird sich mit einem allgemeinem Interace nicht abbilden lassen und somit wird man vermutlich Richtung GUI konkretere Interfaces pro Hardware-Schnittstelle brauchen.

Mmmh, die Idee war eigentlich, dass automatisch nach der Sensorhardware gesucht wird und nix eingestellt werden muss. Im Fall der o.g. seriellen Ports eine feste Baudrate, etc. und im Falle eines Wechsel auf direkte USB-Kommunikation gibt's wahrscheinlich auch nix einzustellen.

Mal abgesehen von den Interfaces die man dafür definiert, der Zugriff sollte ausgelagert und gekapselt sein, was das Testen dieser Schnittstellen erst ermöglicht.

Ich hab eine eigene Klasse für den COM-Port definiert. Hier gibt's ein privates Serialport-Objekt, welches bei Instanzierung nach der Hardware sucht. Öffentlich mache ich nur die o.g. Funktionen über das Interface. Meinst du das mit auslagern und kapseln?

Eine Logik-Klasse kann wahlweise entweder alle Schnittstellen kennen und bedienen oder noch einmal separat pro Schnittstelle existieren - und vlt. noch eine Logik-Klasse darüber haben die diese wieder vereint... Auch wieder aus dem selben Aspekt der Test-und Wartbarkeit. Wenn die Logik sich pro Hardware-Schnittstelle gravierend unterscheidet würd ich teilen, ansonsten reichts wsl. in einer.

Da ich die Erkennung schon ganz unten mache(n möchte), würde ich nur eine Logik implementieren und bei Bedarf eben die untere Schicht (Klasse) austauschen bzw. nebenläufig implementieren. D.h. es gibt später vielleicht mal eine USB-Klasse, die zusätzlich instanziert wird. Wenn die COM-Port Klasse keine Hardware findet, darf die USB-Klasse ran (oder umgekehrt).

Richtung GUI (angenommen du verwendest WPF) hast du dann ein ViewModel pro View die du darstellen willst, welche die notwendigen Properties (für die View) im Sinne von MVVM an die Gui-Elemente bindet. Die ViewModels sind dann für die Interaktion mit den Logics verantwortlich.

Das hatte ich vergessen zu erwähnen, ich bin "klassisch" mit WinForms unterwegs. Ich sehe keine Vorzüge in WPF (wobei ich mich mit tieferen Infos dazu auch nicht weiter beschäftigt habe). Da VS wohl keinen Editor dafür hat und man dafür auch noch XML(?) an der Backe hat, um die GUI zu definieren, war das für mich nicht interessant.

Dein Beispiel mit GetPorts(): ... Unabhängig von WinForms oder WPF, das was du sagst klingt danach, dass der Ansatz schon richtig ist, über diverse Felder oder Eigenschaften bekannt zu machen, welche Einstellungen es gibt. Das werde ich dann so für die verschiedenen Sensortypen implementieren.

Also zusammengefasst schaut dein Ansatz unterhalb der GUI schon ordentlich aus und Richtung GUI schau dir [Artikel] MVVM und DataBinding an und auch den WPF Example Code von OxyPlot an.

Da du durch das Auslagern des Codes in verschiedene Komponenten wsl. viele Klassen inkl. Interfaces erzeugen wirst empfiehlt sich Dependency Injection einzusetzen um die Abhängigkeiten zu reduzieren, das Kompositionieren der Services zu vereinfachen und vor allem um den Code testbarer zu machen.

Okay, zwei neue Begriffe, in die ich erst reinschnuppern muss, Dependency Injection und DataBinding 😃
Was das Testen betrifft - ich habe bisher auch nichts mit irgendwelchen automatisierten Tests o.ä. gemacht. Der Test war bisher immer die Anwendung der Anwendung 😄

Grüße

====================================
1001011010101010101101110101111000101010101010
Ich assimilier dich...
Und dich auch...
Ich mein's ernst!

J
61 Beiträge seit 2020
vor 3 Jahren

Das hatte ich vergessen zu erwähnen, ich bin "klassisch" mit WinForms unterwegs. Ich sehe keine Vorzüge in WPF (wobei ich mich mit tieferen Infos dazu auch nicht weiter beschäftigt habe). Da VS wohl keinen Editor dafür hat und man dafür auch noch XML(?) an der Backe hat, um die GUI zu definieren, war das für mich nicht interessant.

[...]

Okay, zwei neue Begriffe, in die ich erst reinschnuppern muss, Dependency Injection und DataBinding 😃

Databinding geht natürlich auch mit Winforms: Windows Forms Data Binding

1.378 Beiträge seit 2006
vor 3 Jahren

Mmmh, die Idee war eigentlich, dass automatisch nach der Sensorhardware gesucht wird und nix eingestellt werden muss. Im Fall der o.g. seriellen Ports eine feste Baudrate, etc. und im Falle eines Wechsel auf direkte USB-Kommunikation gibt's wahrscheinlich auch nix einzustellen.

Ich hab mich dabei auf dein Kommentar bezogen:

Von der GUI zur Logikschicht, also bspw. für die Einstellungen der Sensoren, hier bin ich unschlüssig, wie das aussehen soll

Ich hab eine eigene Klasse für den COM-Port definiert. Hier gibt's ein privates Serialport-Objekt, welches bei Instanzierung nach der Hardware sucht. Öffentlich mache ich nur die o.g. Funktionen über das Interface. Meinst du das mit auslagern und kapseln?

Jap genau das meinte ich... nur nochmals wiederholt um die Wichtigkeit hervorzuheben 😃

Das hatte ich vergessen zu erwähnen, ich bin "klassisch" mit WinForms unterwegs. Ich sehe keine Vorzüge in WPF (wobei ich mich mit tieferen Infos dazu auch nicht weiter beschäftigt habe). Da VS wohl keinen Editor dafür hat und man dafür auch noch XML(?) an der Backe hat, um die GUI zu definieren, war das für mich nicht interessant.

Ich habe auch mit WinForms angefangen und sehe seit WPF keinen Grund mehr das Zeugs jemals wieder anzugreifen 😃
Es ist sicherlich eine Umstellung aber man arbeitet sich glaub ich da schnell ein da es mMn. wesentlich intuitiver ist als WinForms.

Ein Qualitätskriterium für mich ist wie testbar Code ist (weswegen ich ständig vom Testen rede) - ich arbeite nicht immer (eher selten) nach TDD-Regeln aber zumindest schau ich dass ich alles soweit entkopple, dass jeder entscheidende Code-Pfad ohne Nebeneffekte getestet werden kann. Das begingt wie schon mehrmals erwähnt beim Auslagern von Code in eigene Klassen, weiter noch die Abhängigkeiten der Klassen untereinander durch Interfaces definieren, sodass am Ende jede Klasse beispielsweise nur mit "Fake-Instanzen" getestet werden kann.