ZeroMQ-Schnittstelle
Wie ein Flowgraph über ZeroMQ Daten oder Nachrichten an ein externes Programm sendet — am Beispiel des DCF77-Projekts, das seine dekodierte Uhrzeit an eine eigenständige Qt-Anzeige überträgt.
GNU Radio Companion bietet mit den ZeroMQ-Interfaces einen Satz von Blöcken, die einen Flowgraph über einen Netzwerksocket mit einem beliebigen externen Prozess verbinden — egal ob dieser ebenfalls in GNU Radio läuft oder ein völlig eigenständiges Programm ist. Das darunterliegende ZeroMQ ist eine leichtgewichtige Messaging-Bibliothek, die klassische Socket-Programmierung um fertige Kommunikationsmuster (Publish/Subscribe, Push/Pull, Request/Reply) ergänzt, ohne einen eigenen Broker-Prozess zu benötigen. Die einzelnen Blöcke sind im Block-Verzeichnis beschrieben, gruppiert nach ZMQ PUB, ZMQ SUB, ZMQ REQ, ZMQ REP und ZMQ PULL.
Im DCF77-Projekt wird genau dieses Muster genutzt: Der Flowgraph dekodiert die Zeitinformation und sendet sie per ZMQ PUB Message Sink an tcp://*:5555. Ein separates, unabhängig laufendes Python/Qt-Programm empfängt diese Nachrichten über einen SUB-Socket und zeigt die Uhrzeit grafisch an — siehe DCF77: Demodulation in GNU Radio.

1. Publish/Subscribe: das Grundmuster
Für GRC-Flowgraphs stehen zwei Varianten des PUB/SUB-Musters zur Verfügung, je nachdem, welche Art von Daten übertragen werden soll:
| Block | Datenart | Verwendung |
|---|---|---|
| ZMQ PUB Sink / ZMQ SUB Source | Stream (IQ-Samples, Float-Werte, …) | rohe Abtastwerte an ein anderes Programm (z. B. eine zweite GNU-Radio-Instanz) senden |
| ZMQ PUB Message Sink / ZMQ SUB Message Source | Message (einzelne PMT-Nachrichten über einen Message-Port) | strukturierte, unregelmäßig anfallende Ereignisse senden — z. B. dekodierte Textstrings, wie im DCF77-Beispiel |
Das DCF77-Projekt nutzt die Message-Variante: Der Decoder-Block erzeugt keinen kontinuierlichen Datenstrom, sondern einmal pro erfolgreich dekodiertem 59-Sekunden-Rahmen eine einzelne formatierte Textnachricht. Details zu den einzelnen Blöcken (Parameter, Bind/Connect-Rollen) siehe ZMQ PUB Sink und ZMQ SUB Source im Block-Verzeichnis.

2. Parameter und Dimensionierung
| Parameter | Typ/Einheit | Bedeutung | Dimensionierungshinweis |
|---|---|---|---|
address (Address) |
String | Netzwerkadresse als ZeroMQ-Endpoint, z. B. tcp://*:5555 |
* bindet auf alle lokalen Netzwerkschnittstellen; für reinen Lokalbetrieb reicht tcp://127.0.0.1:5555 |
timeout (Timeout) |
Millisekunden | wie lange auf verfügbare Daten/Nachrichten gewartet wird, bevor der Block einen leeren Zyklus abschließt | Standard 100 ms; beeinflusst primär die Reaktionszeit beim Beenden des Flowgraphs, nicht die Übertragungslatenz |
bind (Connection) |
Bool/Enum | ob der Block den Socket bindet (Server-Rolle) oder sich verbindet (Client-Rolle) | PUB-Blöcke binden per Default (True), SUB-Blöcke verbinden sich per Default (False) — dieses Rollenpaar ist in GRC bereits sinnvoll vorbelegt |
hwm (High Watermark, nur Stream-Variante) |
Integer | maximale Anzahl gepufferter Nachrichten, bevor ZeroMQ neue Daten verwirft oder blockiert | -1 = ZeroMQ-Standardwert; nur bei nachweisbarem Nachrichtenverlust unter Last erhöhen |
pass_tags (nur Stream-Variante) |
Bool | ob Stream-Tags mit über den Socket übertragen werden | „Yes" nur, wenn die Gegenseite ebenfalls GNU Radio ist und Tags auswerten kann |
ZMQ PUB Message Sink, bind=True), die externe Anzeige verbindet sich (socket.connect(...)) — die klassische Server/Client-Aufteilung.3. Der externe Client: dcf77_display.py
Auf der Empfängerseite genügt eine minimale ZeroMQ-SUB-Verbindung — kein GNU-Radio-Import nötig, jede Sprache mit ZeroMQ-Bindings (Python, C++, JavaScript, …) kann als Client dienen. Das DCF77-Projekt nutzt dafür ein eigenständiges PyQt5-Programm:
import zmq
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://127.0.0.1:5555")
socket.setsockopt_string(zmq.SUBSCRIBE, "") # alle Topics abonnieren
import zmq
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://127.0.0.1:5555")
socket.setsockopt_string(zmq.SUBSCRIBE, "") # alle Topics abonnieren
Der leere String bei SUBSCRIBE bedeutet „alle Nachrichten empfangen, unabhängig vom Topic-Präfix" — ZeroMQ filtert PUB/SUB-Nachrichten normalerweise anhand eines Präfix-Strings (dem key-Parameter auf Senderseite), im einfachsten Fall wird dieser Mechanismus hier aber gar nicht genutzt.
Der Empfang selbst läuft nicht-blockierend in einem Qt-Timer, damit die Programmoberfläche parallel reagibel bleibt:
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.update_data)
self.timer.start(100)
def update_data(self):
try:
raw = self.socket.recv(zmq.NOBLOCK)
text = raw.decode('utf-8', errors='ignore')
p = text.split('|') # Zeit|Bits|Datum|Zone|Schaltsek
...
except zmq.Again:
pass # keine neue Nachricht in diesem Zyklus
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.update_data)
self.timer.start(100)
def update_data(self):
try:
raw = self.socket.recv(zmq.NOBLOCK)
text = raw.decode('utf-8', errors='ignore')
p = text.split('|') # Zeit|Bits|Datum|Zone|Schaltsek
...
except zmq.Again:
pass # keine neue Nachricht in diesem Zyklus
zmq.NOBLOCK sorgt dafür, dass recv() sofort mit zmq.Again abbricht, wenn gerade keine Nachricht vorliegt, statt den Qt-Event-Loop zu blockieren. Die 100 ms-Polling-Rate ist unabhängig vom timeout-Parameter des GRC-Blocks — sie bestimmt nur, wie oft die Qt-Anwendung selbst nachschaut, nicht wie GNU Radio intern wartet.
| getrennten String — Zeit|Bits|Datum|Zone|Schaltsek. Das ist bewusst einfach gehalten: kein JSON, kein Binärformat, nur str.split('|') auf beiden Seiten. Für ein Projekt mit einer Handvoll fester Felder ist das ein legitimer, minimalistischer Ansatz — bei mehr oder variablen Feldern wäre ein selbstbeschreibendes Format wie JSON robuster gegen künftige Erweiterungen.4. Stolperstein: PMT-Serialisierung bei der Message-Variante
pmt.intern("12:34:56|30|01.01.2026|MEZ|NEIN") wird auf dem Draht zu b'\x02\x00\x1f12:34:56|30|01.01.2026|MEZ|NEIN' — mit einem 3 Byte langen Binär-Header vor dem eigentlichen Text.Das erklärt eine Eigenheit im gezeigten Client-Code: raw.decode('utf-8', errors='ignore') interpretiert diese Header-Bytes (\x02, \x00, \x1f — alles gültige, aber nicht druckbare UTF-8-Codepoints) einfach mit als Text und hängt sie vor das erste Feld (p[0], die Uhrzeit). Da es sich um nicht darstellbare Steuerzeichen handelt, bleiben sie in der Qt-Anzeige unsichtbar — der Trick funktioniert hier also eher zufällig, weil das erste Feld nirgends auf exakte Gleichheit geprüft oder in eine Zahl umgewandelt wird.
Der robuste Weg, eine ZMQ-PUB-Message-Nachricht in Python zu lesen, nutzt stattdessen PMTs eigene Deserialisierung:
import pmt
raw = socket.recv(zmq.NOBLOCK)
msg = pmt.deserialize_str(raw)
text = pmt.symbol_to_string(msg) # exakt "12:34:56|30|01.01.2026|MEZ|NEIN", ohne Header
import pmt
raw = socket.recv(zmq.NOBLOCK)
msg = pmt.deserialize_str(raw)
text = pmt.symbol_to_string(msg) # exakt "12:34:56|30|01.01.2026|MEZ|NEIN", ohne Header
pmt.deserialize_str(raw) gefolgt von pmt.symbol_to_string(msg) liefert exakt den ursprünglichen String zurück, ganz ohne die führenden Steuerzeichen — der korrekte Gegenpart zur PMT-Serialisierung auf Senderseite. Für einfache, rein textuelle Nachrichten wie im DCF77-Beispiel ist der naive decode()-Ansatz unschädlich; bei numerischen Feldern direkt nach dem Header, bei Nachrichten mit anderem PMT-Typ (z. B. Dictionaries oder Zahlen statt Symbolen) oder bei exakten String-Vergleichen würde er dagegen sichtbar falsche Ergebnisse liefern.5. Weitere wichtige Aspekte
Kein GNU Radio auf der Empfängerseite nötig: Da ZeroMQ ein sprachunabhängiges Wire-Protokoll ist, kann der Client in beliebiger Sprache mit ZeroMQ-Bindings geschrieben werden — die Stream-Varianten übertragen dabei rohe Bytes im jeweiligen Item-Format (z. B. complex64 für IQ-Samples), die Message-Varianten das oben gezeigte PMT-Binärformat.
Mehrere Subscriber gleichzeitig: PUB/SUB ist ein 1-zu-n-Muster — beliebig viele SUB-Clients können sich gleichzeitig mit demselben PUB-Endpoint verbinden und erhalten alle dieselben Nachrichten, ohne dass der Flowgraph davon weiß oder zusätzliche Last durch weitere Zuhörer entsteht.
Kein Nachliefern verpasster Nachrichten: ZeroMQ PUB/SUB puffert nicht rückwirkend — ein Subscriber, der sich erst nach dem Senden verbindet, hat die vorherige Nachricht endgültig verpasst. Für das DCF77-Beispiel unproblematisch (die nächste Minute liefert ohnehin eine neue Nachricht), für Anwendungen mit seltenen, nicht wiederholbaren Ereignissen ggf. relevant.
Vollständiges Beispiel zum Download:
dcf77-zeromq-display.zip — der komplette, hier gezeigte PyQt5-Client (dcf77_display.py), lauffähig gegen den ZMQ-PUB-Sink des DCF77-Flowgraphs.
DCF77: Demodulation in GNU Radio
Der vollständige Flowgraph, dessen ZMQ PUB Message Sink dieses Beispiel speist.
Daten in GRC
Wie Daten sonst innerhalb eines Flowgraphs zwischen Blöcken fließen — Puffer, Scheduler, Backpressure.
Testen mit GRC
Headless-Tests als Alternative zur externen GUI-Anzeige, wenn kein visuelles Feedback nötig ist.