NZXT: Lüfter auch unter Linux steuern

Für Linux-Enthusiasten ist es immer wieder ein Ärgernis: Oftmals werden Treiber und Software nur für Windows entwickelt. Das gilt auch für die Lüftersteuerung Grid+ des Herstellers NZXT. Das habe ich allerdings erst herausgefunden, nachdem ich sie eingebaut hatte, denn leider hatte ich sie ohne weitere Recherche im Angebot bestellt. Die Steuerung wird mit Plug & Play beworben, doch da will Linux nicht mitmachen. Da ich sie nun aber schon einmal habe und mich nicht gleich wieder von ihr trennen will, unternehme ich etwas: Ich mache meine Lüftersteuerung Linux-tauglich - und das ist einfacher als befürchtet.
Ich habe die Lüftersteuerung verbaut, weil ich beim Einbau einer leisen Wasserkühlung für meinen Computer auf ein Problem stieß: Ich brauche fünf Lüfter und eine Pumpe - doch meine Hauptplatine hat nur drei Anschlüsse. Das NZXT-Grid schien die günstigste Lösung zu sein, zumal es im Angebot nur 20 statt rund 40 Euro kostete. Auch ist es leicht einzubauen; die eigentliche Ansteuerung erfolgt über einen USB-Anschluss, die Stromversorgung über einen Molex-Stecker.
Mit Windows klappt es, mit Linux nicht
Nach dem Einbau bootete ich Windows und lud mir die aktuelle Version der Steuerungssoftware herunter. Damit funktionierte alles. Die Oberfläche der Software ist ressourcenhungrig und spricht mich nicht an, aber damit kann ich leben. Die Lüftersteuerung dreht nur dann auf, wenn es notwendig ist.
In Freude über die neu erworbene Ruhe unter meinem Schreibtisch startete ich den PC neu und wählte diesmal das ebenfalls installierte Linux-System. Und schon war es mit der Ruhe vorbei. Die Lüfter drehten sofort auf 100 Prozent hoch. Ein dmesg verrät mir: Es wurde kein Treiber für eine Lüftersteuerung geladen. Stattdessen werde ich von einem neuen seriellen Port begrüßt - vermutlich die Schnittstelle zur Lüftersteuerung. Im Folgenden versuche ich herauszufinden, ob und wie genau ich die Lüftersteuerung darüber ansprechen kann.
Was auf der Platine steckt
Nach kurzer Verwunderung entscheide ich mich, die Lüftersteuerung zu öffnen. Neben sechs identischen Komponentengruppen findet sich auf der Vorderseite ein Chip mit der Kennung MCP2200. Auf der Rückseite sitzen zwei weitere Bauteile mit der Kennung STM800. Mit dem Mikrocontroller STM32 habe ich schon gearbeitet. Eine kurze Suche im Internet ergibt, dass der STM800 eine leistungsschwächere Variante ist. Der Chip mit der Kennung MCP2200 ist ein USB-zu-seriell-Konverter. Deshalb erkennt Linux auch eine neue serielle Schnittstelle, wenn das Grid eingesteckt ist.




Der Softwaretreiber wird analysiert
Offensichtlich werden die Steuerdaten über die serielle Schnittstelle übermittelt. Ich muss herausfinden, was dabei übertragen wird, um selbst Befehle zu übermitteln. Meinen ersten Gedanken, die seriellen Daten auf der Platine selbst abzugreifen und zu analysieren, verwerfe ich vorerst.
Bevor ich mir die Mühe mache, das Protokoll von Hand zu entschlüsseln, schaue ich mir die Software genauer an. Im Installationsverzeichnis finde ich CAMV2.Hardware.dll - eine Erweiterung, die namentlich das macht, was ich unter Linux implementieren möchte.
Die Steuerungssoftware läuft nur unter Windows und hat eine aufwendig designte Benutzerschnittstelle. Der Ressourcenbedarf des Programms ist absurd hoch: Bei mir sind es 800 MB Arbeitsspeicher. Erfahrungsgemäß basiert eine solche Software auf dem .NET-Framework von Microsoft.
Microsoft hilft mit
Grundsätzlich kann jede ausführbare Datei mit Hilfe eines Decompilers analysiert und in eine lesbare Programmiersprache zurückübersetzt werden. Bei .NET geht es sogar einen Schritt weiter: Der ursprüngliche Quellcode lässt sich mit Ausnahme der Kommentare fast vollständig wiederherstellen.
Ich entscheide mich, mein Glück mit dem .NET Decompiler ILSPY(öffnet im neuen Fenster) zu versuchen. Nach dem Öffnen der DLL in ILSPY werde ich mit einer Vielzahl an Klassen begrüßt - und lande einen Volltreffer: GridPlusControl . Das klingt nach dem, was ich suche!




Die Drehzahl steuern
Während ich den Quellcode überfliege, werde ich auf die Funktion SetGridPlusVoltage() aufmerksam. Es werden zwei Funktionen aus weiteren Klassen aufgerufen, die ich mir daraufhin anschaue. Was zunächst komplex aussieht, stellt sich als simpel heraus. Zunächst wird eine ID gesendet, darauf ein Index, gefolgt von dem Dezimalwert 192, gefolgt von zwei Null-Bytes. Warum 192 gesendet wird, ist mir nicht ganz klar, auch die beiden Null-Bytes irritieren mich. Anschließend wird eine Spannung übermittelt. Insgesamt ergibt sich also folgende Kette (hier für 12,4 Volt):
<![CDATA[ [ID, index ,192, 0, 0, 12, 40] ]]>
Die Eingangsspannung liegt in einem Intervall von 0 bis 12,4 Volt. Um diese zu übertragen, wird das vorletzte Byte auf den Wert vor dem Komma gesetzt. Das letzte Byte wird in zwei 4-Bit Blöcke unterteilt, welche die erste und zweite Nachkommastelle aufnehmen.
Die 0,01 Volt-Schritte verwerfe ich, so genau muss ich die Lüfter nicht steuern können. Sauber aufgeschrieben sieht die Prozedur in Python so aus:
def set_voltage(index , voltage):
vsteps = int(voltage*10)
return [ID_SET_VOLTAGE, index
, 192, 0, 0, (vsteps/10), ((vsteps % 10) < 4)]
Die Drehzahl auslesen
Die Drehzahl kann ich über die Spannung regeln. Wenn der Computer unter dem Schreibtisch steht, ist es mühselig, jedes Mal die Verkleidung zu öffnen und zu schauen, ob meine neuen Einstellungen übernommen wurden. Ich schaue mir wieder die Klasse an und finde die Funktion mit dem Namen GetAllRPM() . Das scheint genau das zu sein, was ich brauche. Eine zwei Byte lange Anfrage resultiert in einer fünf Byte langen Antwort.
Die Anfrage:
<![CDATA[ [ID_GET_RPM, 1] ]]>
Drei der fünf Byte scheinen fix zu sein - 60 Prozent Überschuss ist schon eine Leistung! Auswertung der Antwort:
def get_rpm_cmd(index):
return [138, index]
def get_rpm_response(response):
return (int(response[3]) < 8) + int(response[4])
Das Programm testen
Die Basisfunktionen habe ich jetzt, nun kommt der spaßige Teil: das Testen. Dazu baue ich die Steuerung aus meinem PC aus und schließe einen Lüfter an. Dank der USB-Buchse an der Steuerung kann ich sie auch an einen normalen USB-Port anschließen.
In der Klasse MySerialPortProcess finde ich die benötigte Baud-Rate: 4800. Das ist recht niedrig. Bisher bin ich davon ausgegangen, dass diese Klasse auch eine Prüfsumme generiert. Eine solche Funktion ist aber nicht vorhanden. Eine Prüfsumme wird weder übermittelt noch geprüft.
Als Erstes versuche ich, die Drehzahl auszulesen. Ich sende die Anfrage und bekomme ungefähr eine halbe Sekunde später fünf Bytes als Antwort. Mit der oben deklarierten Funktion get_rpm_response() wird mir die korrekte Drehzahl ausgegeben. Perfekt!
Fehler suchen
Beim Setzen der Spannung habe ich weniger Glück. Nachdem ich den Ausgang unter Windows auf 0 Prozent gesetzt habe, ist es mir nicht möglich, die Spannung unter Linux zu setzen. Egal wie oft ich die Spannung auf 12,4 Volt (Maximalwert im Quellcode) setze, der Lüfter dreht sich nicht.
Nach jedem Befehl kommt ein einziges Byte als Antwort. Es scheint also einen Rückgabewert zu geben. Ich finde nach einigem Testen heraus: 0x01 stellt eine gelungene Operation dar, 0x02 einen Fehler. Tritt ein Fehler auf, muss ich meine Lüftersteuerung zuerst von der Stromquelle trennen, um fortzufahren.
Ich versuche, andere Werte für die Spannung zu setzen, beginnend mit 10 Volt. Siehe da: Der Lüfter läuft an. Die 12,4 Volt scheinen hingegen nicht zu funktionieren. Ich setze den Ausgang auf 0 Volt und schaue, wann er reagiert. Beginnend bei 12,4 Volt zähle ich in 0,1 Volt Schritten abwärts. Bei 12V dreht der Lüfter!
Software in Linux integrieren
Die beiden grundlegenden Funktionen stehen. Unter Windows kann man noch den aktuellen Strombedarf auslesen - das stelle ich erst einmal hintenan. Die Standard-Schnittstelle für Sensoren unter Linux sind die hwmon -Kernelmodule. Für einen Raspberry Pi habe ich bereits ein solches Kernelmodul für einen I2C-Temperatur-Sensor geschrieben. Dasselbe habe ich für meine Lüftersteuerung vor.
Nach langer Recherche komme ich zu dem Schluss, dass mir aktuell das nötige Know-how fehlt, um das Kernelmodul zu entwickeln. Das Problem liegt hier beim USB-zu-seriell-Konverter. Ich müsste das bereits bestehende Kernelmodul für die Kommunikation nutzen, finde aber keinerlei Dokumentation, ob dies überhaupt möglich ist. Weder in der Linux-Dokumentation(öffnet im neuen Fenster) noch bei Stackoverflow(öffnet im neuen Fenster) finde ich passende Informationen.
Eine Alternative zum Kernelmodul entsteht
Unter Windows werden alle Geräte von NZXT mit dem hauseigenen CAM-Programm angesprochen. So hat NZXT ihre Steuerungssoftware genannt. Mit der eigentlichen Abkürzung im Industriebereich hat sie wenig gemeinsam. Warum gibt etwas Ähnliches nicht unter Linux? Ich entscheide mich, mein Projekt in Python zu realisieren(öffnet im neuen Fenster) . Ich arbeite sehr gerne mit der Sprache, habe aber noch nie ein größeres Projekt damit umgesetzt.
Ich erstelle eine neue Bibliothek namens nzxt und füge die Klasse Grid hinzu. Hier implementiere ich alle Funktionen und die Übertragung. Als Argument wird nur ein serieller Port übergeben. Hier trennen sich auch schon Linux und Windows und die Vorteile zeigen sich: Unter Windows wird immer nur ein einziges Gerät einer Reihe unterstützt, unter Linux sind es beliebig viele.
Um mehrere Geräte zu unterstützen muss ich eine Möglichkeit zur Konfiguration bieten. Ich entwickle also eine weitere Klasse camservice , die als Service fungiert. Zunächst kümmere ich mich um die Konfiguration. Ich möchte die Möglichkeit haben, beispielsweise mehrere Lüftersteuerungen zu betreiben. In Linux werden in der Regel Konfigurationsdateien im Unterordner /etc abgelegt. Viel Arbeit möchte ich nicht in einen Parser stecken. Meine Konfigurationsdatei basiert deshalb auf JSON und ich ergänze die Option, Kommentare einzufügen. Diese filtere ich dann mit folgender Prozedur heraus:
def cleanupconfig(str):
buffer = ""
for i in str.splitlines():
if '#' in i:
buffer = buffer + i[0:i.find('#')]
else:
buffer = buffer + i
return buffer
Eingelesen werden kann die Konfiguration danach mit json.loads() . Zurückgeliefert wird ein Wörterbuch ( Dictionary(öffnet im neuen Fenster) ), das die in der Konfiguration festgelegten Parameter beinhaltet.
Anhand dieser Parameter wird eine Liste von Geräten erstellt, die zu jedem Zeitpunkt angesteuert oder ausgelesen werden können.
def parseconfig(self, cfg):
for i in cfg.keys():
self.add_device(i, cfg[i])
Um Daten zu übertragen, nutze ich erneut JSON, hier ein Beispiel, um alle Geräte auszulesen:
def getall(self):
returndict = {}
for i in range(len(self.devices)):
returndict[self.devices[i]["name"]] = self.getdevice(i)
return json.dumps(returndict)
Jetzt habe ich eine Basisstruktur, eine Klasse für die Lüftersteuerung und eine Klasse, die alle Geräte verwaltet. Was jetzt noch fehlt, ist eine Möglichkeit für die Kommunikation mit der Außenwelt. Diese realisiere ich vorerst mit einer einfachen Netzwerkschnittstelle:
# Create camservice instance
c = nzxt.Camservice("/etc/cam4linux/config.json")
# Create socket for server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
…
while True:
conn, addr = s.accept()
try:
…
Die Verbindung zum Nutzer
Der Hintergrunddienst steht. Jetzt möchte man natürlich nicht immer manuell Daten an den Port senden. Ich erweitere das System um eine Kommandozeilen-Anwendung, mit der die Daten aller angeschlossenen Lüftersteuerungen in einem sensorähnlichen Format ausgegeben und zusätzlich die Drehzahl gesteuert werden kann. Diese Kommandozeilen-Schnittstelle macht nichts anderes, als übergebene Argumente korrekt zu kodieren und eine Anfrage an den Hintergrunddienst zu senden.
Fazit
Meine neue Lüftersteuerung Linux-tauglich zu machen, war nicht so schwer, wie ich mir anfangs vorgestellt hatte. Würde Microsoft das .NET-Framework in Linux integrieren, wäre es ein versteckter Ordner. Zurecht - die einfache Dekompilierung hat es mir möglich gemacht, ohne große Mühe das Protokoll zu entschlüsseln. Hätte NZXT beispielsweise C++ als Basis genommen, hätte ich das Protokoll manuell analysieren müssen. Hier ein großes Dankeschön an Microsoft: Ihr habt mir sehr viel Arbeit erspart!
Auch wenn ich auf eine Integration außerhalb des Linux-Kernel ausweichen musste, bin ich zufrieden mit dem Ergebnis. Ein etwas größeres Projekt in Python zu entwickeln, hat mir Spaß gemacht und Python hat mich erneut begeistert.
Sobald ich Zeit dazu finde, werde ich mich erneut an einem Kernelmodul versuchen!
Matthias Riegler studiert IT-Sicherheit und arbeitet neben dem Studium bei Coptersystems. In seiner Freizeit begeistert er sich für diverse größere und kleinere Projekte im Bereich Software- und Hardware-Entwicklung und Fotografie, über die er unter anderem auch in seinem Blog blog.xvzf.tech(öffnet im neuen Fenster) berichtet.



