Hinzufügen neuer Porttypen - nkaeming/ILPLabWatch GitHub Wiki
Aufgaben von Ports
Portklassen sind die lesende Schnittstelle zwischen ILPLabWatch und der Hardware des Raspberry Pi. Portklassen stellen benutzerdefinierte Methoden bereit um:
- Sensordaten zu erfassen
- Einstellungsmöglichkeiten zu definieren
- Das Leseverhalten zu beeinflussen
Schritt 0: Vorbereitung
Bevor die Entwicklung von Postklassen beginnen kann, müssen folgende Dinge überlegt werden:
- Wie wird der Sensor von Python angesteuert?
- Wie häufig ist es technisch möglich den Sensor anzusteuern?
- Handelt es sich um ein Bus-System (z.B. OneWire)? Können mehrere physische Sensoren von einer Portklasse (z.B. Digital Analog Wandler) angesteuert werden?
- Ist der Port selbst reaktiv, kann er also ohne zeitlich wiederholenden Aufruf Veränderungen mitteilen? (Betrachte Hierzu das Entwurfsmuster Observer-Listener) Ein Beispiel sind die einfachen IO-Ports
Schritt 1: Erstellen der Dateistruktur
In dem Ordner Ports/UserPorts werden die Python Module für selbst entwickelte Ports gespeichert. Für den neuen Port muss ein Ordner mit dem gewünschten Namen bereitgestellt werden. Portnamen bestehen aus Buchstaben (keine Umlaute) und ggf. Zahlen. Idealerweise folgen sie der CamelCase Schreibweise (mehr).
In dem Ordner muss eine leere Pythondatei mit dem Namen __init__.py
erstellt werden. Sie sorgt dafür, dass das System den Ordner als Modul erkennt.
Für bestimmte Regeln in der Benutzeroberfläche muss eine Datei options.cfg
erstellt werden. In ihr muss zunächst {}
ein leeres Python dict bzw. eine leere JSON Struktur erzeugt werden.
Die wichtigste Datei im Ordner ist die PortName.py. WICHTIG: Der Name dieser Datei muss genau (Case Sensitiv) dem Ordnernamen entsprechen. Sie kann zunächst leer gelassen werden.
Schritt 2: Vorbereiten der Portklasse
Bevor die eigentliche Portlogik implementiert werden kann, muss eine Klassenstruktur geschaffen werden. Dazu wird in einer IDE/Editor die Datei PortName.py geöffnet und folgender Inhalt eingefügt:
from Ports.AbstractPort import AbstractPort
class PortName(AbstractPort):
def __init__(self, settings, id):
super().__init__(settings, id)
self.minRefreshTime = 5
self.afterInit()
Natürlich muss PortName durch den Portnamen ersetzt werden. ACHTUNG: Er muss dem Ordnernamen und dem Dateinamen entsprechen!
Der import importiert die abstrakte Portklasse, die bereits einige wesentliche Funktion für das System enthält. Diese wollen wir durch eigenen Code erweitern.
Die init Methode wird beim erzeugen eines Objektes aus unserer Portklasse aufgerufen. Hier können z.B. GPIO Pins auf in gesetzt werden o.Ä. Es ist unbedingt notwendig von hier den Konstruktor der Superklasse (also in dem Fall AbstractPort) aufzurufen. self.minRefreshTime
setzt die Zeit in Sekunden fest, die zwischen zwei Portabfragen besteht. self.afterInit()
wird nach der möglichen GPIO Konfiguration ausgeführt. Es ruft an der Klasse AbstractPort eine Methode auf, die das Logging und die Überwachung des Ports startet. Eigentlich sollte diese Methode immer am Ende der Initialisierung aufgerufen werden.
Schritt 3: Schreiben der Portlogik
Im einfachsten Fall soll hier anhand eines Beispiels gezeigt werden, die das implementieren eines Ports abläuft, welcher in festgelegten Zeitintervallen abgefragt wird und kein Bus-System o.Ä. beschreibt. Für Informationen zu diesen Ports, sollte die Port-Reference (mehr) gelesen werden.
Angenommen wir wollen einen Port erstellen, welcher einen vorher vom Benutzer eingestellten Wert ausgibt. Nennen wir diesen Port im folgenden FesteZahl (Wenig sinnvoll, aber geht auf die wesentlichen Aspekte ein.)
Erstellen der Methode zum Sensor auslesen
Die Sensorlogik wird in der Methode getPrivateState
implementiert. Sie hat den Rückgabetyp int oder float und hat keine Parameter.
In unserem Fall soll hier einfach der vom Benutzer in der UI eingegebene Wert zurückgegeben werden. Die Portklasse sollte nun so aussehen:
from Ports.AbstractPort import AbstractPort
class FesteZahl(AbstractPort):
"""Ein toller Port, welche eine vom Benutzer eingegebene Zahl zurück gibt."""
def __init__(self, settings, id):
super().__init__(settings, id)
self.minRefreshTime = 5
self.nachInit()
def getPrivateState(self):
"""Gibt den aktuellen State des Ports zurück."""
return self.getSetting('usernumber')
Hier wurde die Methode getPrivateState eingefügt. Der Wert der zurückgegeben wird, ist der der Einstellung usernumber. Diese Zahl wurde vom Benutzer vorher in der UI festgelegt. Um das Speichern und auslesen dieser Einstellung braucht man sich an dieser Stelle keine Gedanken machen. Allerdings muss dem System mitgeteilt werden, welche Art von Einstellung dies ist. Dazu mehr im nächsten Abschnitt.
Benutzereinstellungen definieren
Fast immer ist es nützlich, dass der Benutzer einige Parameter zum Port einstellen kann. Dies kann z.B. eine Skalierungskonstante, eine Verhaltensauswahl usw. sein. Damit diese Einstellungen im UI angezeigt werden können und später in der Portklasse bereit stehen, werden diese in der config.cfg definiert.
In unserem Fall möchten wir dem Benutzer die Möglichkeit geben eine Zahl einzustellen. Ob diese Einstellung möglich ist, kann hier nachgelesen werden. Offenbar eignet sich der Optiontyp number für unseren Zweck. Die Datei options.cfg sollte nun so aussehen:
{
"usernumber": {
"type":"number",
"min":1,
"max":100,
"resolution":0.1,
"description": "Eine tolle Zahl, die der Port immer zurück geben soll.",
"name": "Zahl",
"standard": 1,
"tab": 1,
"required": true
}
}
Die Bedeutung der verschiedenen Einstellungen kann hier nachgelesen werden. Wichtig in unserem Fall ist, dass diese Einstellung nun bei der Erstellung von einem Port in der UI abgefragt wird und wir diese in der Portklasse abrufen können.
Zwei weitere notwendige Methoden
Es gibt noch zwei kleine Methoden, die eine Portklasse implementieren muss.
Das System muss wissen in welchem Wertebereich sich die Messdaten des Ports bewegen. Dazu wird die Methode getValueRange
implementiert. Als Rückgabewert hat sie ein array like Objekt. An Position 0 steht der minimale Wert als float oder int, an der Stelle 1 steht der maximale Wert als float oder int und bei Position 2 steht die Schrittweite zwischen zwei möglichen Werten. (Natürlich kann diese Methode auch Logikelemente enthalten. Für gewöhnlich genügt aber eine Liste mit [min,max,step])
Für die UI muss eine Methode getDescription
implementiert werden. Sie hat als Rückgabetyp einen String und gibt einfach eine knappe Beschreibung des Ports zurück.
Unsere Portklasse sollte nun so aussehen:
from Ports.AbstractPort import AbstractPort
class FesteZahl(AbstractPort):
"""Ein toller Port, welche eine vom Benutzer eingegebene Zahl zurück gibt."""
def __init__(self, settings, id):
super().__init__(settings, id)
self.minRefreshTime = 5
self.nachInit()
def getPrivateState(self):
"""Gibt den aktuellen State des Ports zurück."""
return self.getSetting('usernumber')
def getValueRange(self):
"""Gibt den Wertebereich des Ports zurück"""
return [1,100,0.1]
def getDescription(self):
return "Ein Port, der einen vom Benutzer eingegebenen Wert zurück gibt."
Schritt 4: Wiring einstellen
In dem gezeigten Beispiel spielt es zwar keine Rolle, werden reale Ports erzeugt, ist die Information über den Anschluss am Raspberry Pi jedoch unabdingbar. Die Ports und ihre möglichen Anschlüsse sind in der Datei conf/wiringConf.cfg festgelegt.
Zu Beginn sollte diese Datei so aussehen:
{
"SystemPorts": {
"statusLED": "20",
"warnLED": "21"
}
}
Sie besteht aus Schlüsseln und Werten. Die Schlüssel haben den gleichen Namen wie die Portklasse und die Werte sind wiederum Python dicts. Möchten wir nun, dass unser oben erstellte Port Anschlüsse zugewiesen bekommt, kann dies so aussehen:
{
"SystemPorts": {
"statusLED": "20",
"warnLED": "21"
},
"FesteZahl": {
"FEST1": "1",
"FEST2": "2"
}
}
FEST1 und FEST2 sind Namen für die Benutzeroberfläche. 1 und 2 können z.B. Nummern für die GPIO Pins sein, oder Zeichenketten, welche von der Portklasse als Anschluss interpretiert werden. (betrachte auch dynamische Ports oder Bus-Systeme, dazu mehr in der Port Reference).
Um in unserer Portklasse auf diese Information zugreifen zu können, wird die Hilfsmethode getInternalPin()
angeboten. Möchten wir also, dass unsere feste Zahl mit der Anschlussnummer multipliziert wird, könnte unsere Portklasse so aussehen:
from Ports.AbstractPort import AbstractPort
class FesteZahl(AbstractPort):
"""Ein toller Port, welche eine vom Benutzer eingegebene Zahl zurück gibt."""
def __init__(self, settings, id):
super().__init__(settings, id)
self.minRefreshTime = 5
self.nachInit()
def getPrivateState(self):
"""Gibt den aktuellen State des Ports zurück."""
return self.getSetting('usernumber') * self.getInternalPin()
def getValueRange(self):
"""Gibt den Wertebereich des Ports zurück"""
return [1,100,0.1]
def getDescription(self):
return "Ein Port, der einen vom Benutzer eingegebenen Wert zurück gibt."
Hat der Benutzer z.B. einen Port an Anschluss FEST1 erzeugt und die Zahl 2 eingegeben, so wird der Port den Wert 2*1 = 2
zurück geben.