Damit Letzteres klappt, registriert die Software sogenannte Callbacks, also Funktionen, die die Kommandos und Tastendrücke abfangen. Libcec ruft diese auf, wenn entsprechende Ereignisse auf dem Bus eintreten.
Für den Einsatz der Python-Schnittstelle gibt es allerdings keine brauchbare Dokumentation. Das Interface ist aber eine fast automatisch generierte Hülle zu den C++-Funktionen, deshalb orientieren Sie sich für die implementierten Methoden am einfachsten an der Quelldatei libcec/src/libcec/CECClient.h, ungefähr ab Zeile 260.
Die Methoden gleichen sich und besitzen unter Python dieselben (unpythonischen) Namen. Zusammen mit dem Programm aus libcec/src/pyCecClient/pyCecClient.py und dem darauf basierenden Beispiel in diesem Artikel kopieren Sie sich die notwendigen Teile schnell zusammen. Alternativ rufen Sie innerhalb Python die verfügbaren Methoden und Konstanten ab:
import cec dir(cec) dir(cec.ICECAdapter)
Den Rahmen sehen Sie in Ausschnitten in Listing 4 – den kompletten Quelltext gibt es als cec-test.py im Verzeichnis tools im Github-Projekt [3]. Der Konstruktor ab Zeile 8 füllt das Konfigurationsobjekt, darunter die Callbacks in den Zeilen 16 bis 18. Anschließend erzeugt er ein Objekt vom Typ ICECAdapter, das die ganze Logik kapselt. Unsere Callbacks, etwa process_keys() ab Zeile 36, geben die Kommandos und Tasten im Beispielprogramm einfach aus, später enthalten diese Methoden den Code der Applikation.
Listing 4
#!/usr/bin/python
import cec, readline
class CecController:
# --- constructor
def __init__(self):
self.log_level = cec.CEC_LOG_WARNING
self.cecconfig = cec.libcec_configuration()
self.cecconfig.strDeviceName = "simple-radio"
self.cecconfig.bActivateSource = 0
self.cecconfig.deviceTypes.Add(cec.CEC_DEVICE_TYPE_TUNER)
self.cecconfig.clientVersion = cec.LIBCEC_VERSION_CURRENT
self.cecconfig.SetLogCallback(self.process_logmessage)
self.cecconfig.SetKeyPressCallback(self.process_key)
self.cecconfig.SetCommandCallback(self.process_command)
self.controller = cec.ICECAdapter.Create(self.cecconfig)
print("libCEC version " +
self.controller.VersionToString(self.cecconfig.serverVersion) +
" loaded: " + self.controller.GetLibInfo())
# search for adapters
self.com_port = self.get_com_port()
if self.com_port == None:
raise EnvironmentError((1,"Kein CEC-Adapter gefunden"))
if not self.controller.Open(self.com_port):
raise EnvironmentError((2,"konnte CEC-Adapter nicht öffnen"))
# --- process key presses
def process_key(self, key, duration):
print("Taste: " + str(key))
return 0
# --- process commands
def process_command(self, cmd):
print("Kommando: " + cmd)
return 0
# --- return com port path of adapter
def get_com_port(self):
for adapter in self.controller.DetectAdapters():
print("CEC Adapter:")
print("Port: " + adapter.strComName)
print("vendor: " + hex(adapter.iVendorId))
print("Produkt: " + hex(adapter.iProductId))
return adapter.strComName
print("Keinen Adapter gefunden")
return None
# --- send an active source message
def set_active(self):
self.controller.SetActiveSource()
# --- send a standby command
def send_standby(self):
self.controller.StandbyDevices(cec.CECDEVICE_BROADCAST)
# --- send mute command
def toggle_mute(self):
self.controller.AudioToggleMute()
# --- increase volume
def volume_up(self):
self.controller.VolumeUp()
# --- run and process commands
def run(self):
while True:
command = raw_input("Kommando: ").lower()
if command == 'q' or command == 'quit':
print('Beende das Programm...')
return
elif command == 'info':
self.print_addresses()
elif command == 'as' or command == 'activesource':
self.set_active()
elif command == 'standby':
self.send_standby()
elif command == 'scan':
self.scan_bus()
elif command == 'mute':
self.toggle_mute()
elif command == 'volup':
self.volume_up()
elif command == 'voldown':
self.volume_down()
elif command[:2] == 'tx':
self.send_command(command[3:])
if __name__ == '__main__':
# initialise main object
controller = CecController()
controller.run()
Das Hauptprogramm besteht aus einer Endlosschleife in der Methode run() (ab Zeile 81) und fragt Befehle auf der Kommandozeile ab, die es dann direkt per ICECAdapter-Objekt auf den Bus schickt. Asynchron ruft dagegen die CEC-Laufzeit die Callbacks auf. Mit den Kommandos testen Sie wieder die Kommandos vom Pi zu den anderen Geräten auf dem Bus. Die Callbacks zeigen dagegen, ob andere Geräte sauber mit dem Mini-PC kommunizieren.
Ergebnisse
Der AV-Receiver von Denon etwa schickt keine Tasten an den Pi, die Methode process_key() liegt brach. Der Samsung-Fernseher leitet dagegen die gedrückten Tasten an den Pi weiter, falls dieser die aktive Quelle ist. Das bedeutet für das Beispiel, dass das eigentlich gewünschte Steuern des Webradios über die Fernbedienung des Receivers nicht funktioniert. Und den Fernseher anzuschalten, nur um das Radio zu steuern, ist keine Lösung.
Denon hat an einer anderen Stelle den CEC-Standard ebenfalls nicht richtig verstanden, denn der Receiver sendet nicht nur keine Tasten, sondern darüber hinaus falsche Befehle auf den Bus.
Nach einem Befehl zum Umschalten (etwa von der Quelle “DVD” auf “Media-Player”) schickt das Gerät die Änderung per Broadcast an alle angeschlossenen Geräte. Der passende Callback sieht etwa den Befehl "5f:80:20:00:22:00" und den entschlüsselt CeC-o-Matic (Abbildung 3) als: Das Gerät 5 (der Receiver) sendet an alle Geräte den Befehl “80” (Änderung der Quelle).
Dummerweise sind die mitgelieferten Parameter, die alte und die neue Adresse (20:00 und 22:00), allerdings immer wieder mal falsch – es ist schlicht nicht möglich, sich darauf zu verlassen und daraus Aktionen abzuleiten.
Wer also die volle Funktionalität einer fremden Fernbedienung in seinen Programmen nutzen will, kommt um ein bisschen Reverse-Engineering nicht herum. Die oben genannte CeC-o-Matic-Webseite hilft dabei genauso wie die Datei libcec/include/cectypes.h. Die dort definierten symbolischen Konstanten gibt es ebenfalls im Python-Paket, so ist etwa CEC_OPCODE_ROUTING_CHANGE mit 0x80 definiert.
Als Trostpflaster blieb im Selbstversuch, dass der Pi die Lautstärke einschließlich Mute direkt an den Receiver weiterleitete. Das spart zwar keine Fernbedienung auf der Couch, aber zumindest das Bedienen ist einfacher.
Fazit
Das Beispiel CEC zeigt eindrucksvoll, dass nur wenige Zeilen Python-Code für die Integration eines RasPi in die eigene Landschaft an Heimelektronik genügen. Allerdings entspricht dies nur der halben Miete, da einige Geräte anderer Hersteller eine nahtlose Integration versauen.
Statt eine offene Firmware zu verwenden, bei der jedermann die Möglichkeit hätte, einen Fehler zu beheben, kocht jeder Hersteller sein eigenes Süppchen und liefert das Gericht dann noch halb gar aus. Die Entscheidung, Netzwerkfunktionen lieber durch einen RasPi nachzurüsten als im Gerät integriert zu kaufen, war da nur folgerichtig.






