Probe-Blöcke im Überblick

Acht Blöcke, die einen Wert aus dem laufenden Flowgraph nach Python (oder nach außen) durchreichen, statt ihn selbst anzuzeigen — mit grundlegend unterschiedlichen Zugriffsmechanismen und Rückgabeformaten.

Alle „Probe"-Blöcke haben eines gemeinsam: Sie hängen als Sackgasse (kein oder kaum ein Ausgang) an einem Signalpfad und machen dessen aktuellen Zustand für eigenen Python-Code zugänglich — genau das Muster, das im Kivy-Frontpanel-Projekt für Spektrum, Zeitsignal und Frequenzanzeige verwendet wird. Sie unterscheiden sich aber deutlich darin, wie der Wert abgeholt wird und was genau er enthält.

1. Zwei grundlegend verschiedene Zugriffsmuster

Muster Blöcke Wie der Wert ankommt
Pull (synchroner Methodenaufruf) Probe Signal, Probe Signal Vector, Probe Avg Mag^2, Probe Density eigener Code ruft eine Getter-Methode (.level(), .density()) auf und bekommt den zuletzt gespeicherten Wert zurück
Push (asynchrone PMT-Nachricht) Probe Rate der Block sendet selbstständig in festen Abständen eine Nachricht an einen verbundenen Message-Port
Extern über ControlPort Ctrlport Probe, Ctrlport Complex Probe der Wert wird per Thrift-RPC nach außen exportiert, für einen separaten ControlPort-Client — kein Python-Methodenaufruf im selben Prozess
Meta-Block Function Probe kein eigener Signaleingang — pollt stattdessen periodisch eine beliebige Python-Methode eines anderen Blocks und schreibt das Ergebnis in eine GRC-Variable

Das Pull-Muster ist dasjenige, das im Kivy-Projekt verwendet wird (siehe dort Abschnitt 2.2/2.3): Clock.schedule_interval ruft .level() synchron aus dem Kivy-Hauptthread ab, sicher über die Thread-Grenze hinweg, weil die Probe-Blöcke genau dafür gebaut sind.

2. Probe Signal

Der einfachste Probe-Block: ein einzelner Skalarwert, keine Mittelung, keine Vektorisierung. .level() liefert exakt das zuletzt verarbeitete Sample zurück — sobald ein neues Sample eintrifft, wird der alte Wert überschrieben.

Probe Signal Block

Parameter Bedeutung
type (Input Type) Complex/Float/Int/Short/Byte — bestimmt sowohl den erwarteten Eingangsdatentyp als auch den Python-Klassennamen (probe_signal_c/f/i/s/b)

Inhalt der Variable: ein einzelner Python-Skalar (float, complex oder int, je nach type) — kein Array, kein Vektor.

Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt):
from gnuradio import gr, blocks

tb = gr.top_block()
src = blocks.vector_source_f(list(range(1, 21)), repeat=False)  # 1.0 ... 20.0
probe = blocks.probe_signal_f()
tb.connect(src, probe)
tb.run()

print(type(probe.level()), probe.level())
# <class 'float'> 20.0   -- exakt das letzte verarbeitete Sample
from gnuradio import gr, blocks

tb = gr.top_block()
src = blocks.vector_source_f(list(range(1, 21)), repeat=False)  # 1.0 ... 20.0
probe = blocks.probe_signal_f()
tb.connect(src, probe)
tb.run()

print(type(probe.level()), probe.level())
# <class 'float'> 20.0   -- exakt das letzte verarbeitete Sample

3. Probe Signal Vector

Dieselbe Idee wie Probe Signal, aber für vektorwertige Eingänge (z. B. den Ausgang eines Log Power FFT- oder Stream to Vector-Blocks). .level() liefert immer den kompletten zuletzt empfangenen Vektor auf einmal zurück — genau der Block, der im Kivy-Frontpanel für Spektrum und Zeitsignal verwendet wird.

Probe Signal Vector Block

Parameter Bedeutung
type Complex/Float/Int/Short/Byte, wie bei Probe Signal
vlen (Vector Length) Länge eines Vektors — muss zur vlen des Eingangs passen (z. B. fft_size bei einem Log Power FFT)

Inhalt der Variable: eine Python-Liste (bzw. ein listenartiges Objekt) mit genau vlen Elementen — der jeweils zuletzt vollständig eingetroffene Vektor. Ältere Vektoren werden beim nächsten Eintreffen vollständig überschrieben, nicht angehängt.

Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt):
from gnuradio import gr, blocks

tb = gr.top_block()
data = [float(i) for i in range(24)]              # 0.0 ... 23.0
src = blocks.vector_source_f(data, repeat=False)
s2v = blocks.stream_to_vector(gr.sizeof_float, 8)
probe = blocks.probe_signal_vf(8)
tb.connect(src, s2v, probe)
tb.run()

print(type(probe.level()), len(probe.level()), list(probe.level()))
# <class 'list'> 8 [16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0]
# -- die letzte vollstaendige 8er-Gruppe, nicht der Gesamtstream
from gnuradio import gr, blocks

tb = gr.top_block()
data = [float(i) for i in range(24)]              # 0.0 ... 23.0
src = blocks.vector_source_f(data, repeat=False)
s2v = blocks.stream_to_vector(gr.sizeof_float, 8)
probe = blocks.probe_signal_vf(8)
tb.connect(src, s2v, probe)
tb.run()

print(type(probe.level()), len(probe.level()), list(probe.level()))
# <class 'list'> 8 [16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0]
# -- die letzte vollstaendige 8er-Gruppe, nicht der Gesamtstream

Genau dieser Rückgabetyp ist der Grund, warum spectrum_widget.py im Kivy-Projekt die Werte explizit mit np.asarray(..., dtype=np.float32) in ein NumPy-Array umwandelt, statt sie direkt an matplotlib zu übergeben: Kivy-Properties (ListProperty) sind selbst „beobachtbare" Python-Listen mit eigener Callback-Maschinerie — reicht man sie ungewandelt an matplotlib durch, das intern copy.copy() auf den Werten aufruft, kollidiert das mit genau dieser Kivy-internen Beobachtung und führt zu einer RecursionError.

4. Probe Avg Mag^2

Anders als die beiden vorherigen Blöcke liefert dieser keinen Rohwert, sondern einen mit einem Einpol-IIR-Filter exponentiell geglätteten Mittelwert von |x|² — dieselbe Technik, die im Frontpanel-Artikel für das S-Meter verwendet wird.

Probe Avg Mag^2 Block

Parameter Bedeutung
type Complex, Float oder „Complex→Float" (komplexer Eingang, aber float-typisierter zusätzlicher Ausgang)
threshold (Threshold (dB)) Schwelle für den optionalen zweiten Ausgang (1.0/0.0, je nachdem ob der geglättete Pegel darüber liegt) — wirkt sich nicht auf .level() aus
alpha (Alpha) Glättungsfaktor des Einpol-IIR-Filters; kleiner = träger, glatter; größer = schneller, verrauschter

Inhalt der Variable: ein einzelner float — der geglättete Mittelwert der linearen Leistung (|x|²), nicht in dB, obwohl der Parameter „Threshold (dB)" heißt. Für eine dB-Anzeige muss selbst noch 10·log10(...) gebildet werden.

Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt):
from gnuradio import gr, blocks, analog

tb = gr.top_block()
amplitude = 0.5
src = analog.sig_source_f(48000, analog.GR_COS_WAVE, 1000, amplitude, 0)
head = blocks.head(gr.sizeof_float, 48000)   # 1 Sekunde
probe = analog.probe_avg_mag_sqrd_f(0, 2e-3)
tb.connect(src, head, probe)
tb.run()

print(probe.level())
# 0.12488172224428769
# erwartet: amplitude^2 / 2 = 0.25 / 2 = 0.125  (Mittelwert von cos^2 ueber eine volle Periode)
from gnuradio import gr, blocks, analog

tb = gr.top_block()
amplitude = 0.5
src = analog.sig_source_f(48000, analog.GR_COS_WAVE, 1000, amplitude, 0)
head = blocks.head(gr.sizeof_float, 48000)   # 1 Sekunde
probe = analog.probe_avg_mag_sqrd_f(0, 2e-3)
tb.connect(src, head, probe)
tb.run()

print(probe.level())
# 0.12488172224428769
# erwartet: amplitude^2 / 2 = 0.25 / 2 = 0.125  (Mittelwert von cos^2 ueber eine volle Periode)

5. Probe Rate

Misst nicht den Signalinhalt, sondern den tatsächlichen Durchsatz (Samples pro Sekunde) am Eingang — nützlich, um zu prüfen, ob ein Flowgraph wirklich in Echtzeit läuft. Anders als alle bisherigen Blöcke liefert er den Wert nicht über .level(), sondern als asynchrone PMT-Nachricht am Message-Port rate.

Probe Rate Block

Parameter Bedeutung
type / vlen Datentyp und Vektorlänge des Eingangs (nur zur Bestimmung der Item-Größe, kein Einfluss auf das Ergebnisformat)
mintime (Min Update Time (ms)) Mindestabstand zwischen zwei Nachrichten
alpha (Update Alpha) Glättungsfaktor für den mitgelieferten rate_avg-Wert
name (Name) frei wählbarer Bezeichner, taucht als name-Feld in jeder Nachricht wieder auf

Inhalt der Nachricht: kein Skalar und keine Liste, sondern ein PMT-Dictionary mit drei Feldern — name (der konfigurierte Bezeichner), rate_now (aktuell gemessene Rate) und rate_avg (geglättete Rate, gesteuert über alpha). Zum Empfang braucht es einen verbundenen Message-Sink oder einen eigenen Message-Handler (message_port_register_in + set_msg_handler), kein einfacher Attributzugriff.

Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt, mit Throttle auf 48 000 Samples/s begrenzt):
tb.msg_connect(probe, "rate", msg_sink, "print")
...
# ******* MESSAGE DEBUG PRINT ********
# ((name . meine_rate) (rate_now . 47991.8) (rate_avg . 47991.8))
# ************************************
# ******* MESSAGE DEBUG PRINT ********
# ((name . meine_rate) (rate_now . 47350.7) (rate_avg . 47895.6))
# ************************************
tb.msg_connect(probe, "rate", msg_sink, "print")
...
# ******* MESSAGE DEBUG PRINT ********
# ((name . meine_rate) (rate_now . 47991.8) (rate_avg . 47991.8))
# ************************************
# ******* MESSAGE DEBUG PRINT ********
# ((name . meine_rate) (rate_now . 47350.7) (rate_avg . 47895.6))
# ************************************

Die gemessene Rate liegt wie erwartet nahe an den konfigurierten 48 000 Samples/s des Throttle-Blocks.

6. Probe Density

Misst die Dichte von Einsen in einem entpackten Bitstrom (ein Byte pro Bit, Wert 0 oder 1) — z. B. um zu prüfen, ob ein Taktrückgewinnungs- oder Entscheider-Block im Mittel eine plausible 50/50-Verteilung liefert.

Probe Density Block

Parameter Bedeutung
alpha (Alpha) Glättungsfaktor des Einpol-IIR-Filters, wie bei Probe Avg Mag^2
probe_rate (Probe Rate) im GRC-Dialog vorhanden, wird aber vom generierten Code nicht verwendet — der Aufruf lautet schlicht digital.probe_density_b(alpha). Ein totes GUI-Feld, keine Funktionsänderung.

Inhalt der Variable: ein einzelner float zwischen 0 und 1 — der geglättete Anteil der als „1" eingetroffenen Bytes. Wichtig: Der Eingang muss ein entpackter Bitstrom sein (jedes Byte trägt genau ein Bit, Wert 0 oder 1), keine gepackten Datenbytes.

Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt):
from gnuradio import gr, blocks, digital

tb = gr.top_block()
pattern = [1, 0] * 5000                       # 50 % Einsen
src = blocks.vector_source_b(pattern, repeat=False)
probe = digital.probe_density_b(0.01)
tb.connect(src, probe)
tb.run()
print(probe.density())   # 0.4974874371859307

tb = gr.top_block()
pattern = ([1] * 8 + [0] * 2) * 1000           # 80 % Einsen
src = blocks.vector_source_b(pattern, repeat=False)
probe = digital.probe_density_b(0.01)
tb.connect(src, probe)
tb.run()
print(probe.density())   # 0.791880026659935
from gnuradio import gr, blocks, digital

tb = gr.top_block()
pattern = [1, 0] * 5000                       # 50 % Einsen
src = blocks.vector_source_b(pattern, repeat=False)
probe = digital.probe_density_b(0.01)
tb.connect(src, probe)
tb.run()
print(probe.density())   # 0.4974874371859307

tb = gr.top_block()
pattern = ([1] * 8 + [0] * 2) * 1000           # 80 % Einsen
src = blocks.vector_source_b(pattern, repeat=False)
probe = digital.probe_density_b(0.01)
tb.connect(src, probe)
tb.run()
print(probe.density())   # 0.791880026659935

Beide gemessenen Werte liegen wie erwartet nahe an den tatsächlichen Bitanteilen (0,5 bzw. 0,8) — die kleine Abweichung stammt vom Einschwingverhalten des Glättungsfilters bei alpha=0,01.

7. Ctrlport Probe & Ctrlport Complex Probe

Beide Blöcke exportieren Werte nicht in den eigenen Python-Prozess, sondern über GNU Radios ControlPort — einen in den Flowgraph eingebetteten Thrift-RPC-Server. Gedacht für externe Monitoring-Werkzeuge, die sich von außen (auch über das Netzwerk) auf den laufenden Flowgraph aufschalten, nicht für den Zugriff aus demselben Skript heraus.

Ctrlport Probe Block
Ctrlport Complex Probe Block

Parameter Bedeutung
type (nur Ctrlport Probe) Datentyp des Eingangs (Complex/Float/Int/Short/Byte) — Ctrlport Complex Probe ist fest auf Complex spezialisiert
name / desc Bezeichner und Beschreibung, unter denen der Wert im ControlPort-Verzeichnis erscheint
len (nur Ctrlport Probe) Anzahl der auf einmal übertragenen Samples
disp_mask (Display Mask, nur Ctrlport Probe) Hinweis an den ControlPort-Client, als was der Wert dargestellt werden soll: Constellation, Time, PSD, Spectrogram oder Raster

Inhalt der Variable: kein direkt in Python auslesbarer Wert — die Daten liegen im ControlPort-Verzeichnis des laufenden Prozesses und müssen von einem separaten Thrift-Client abgeholt werden. Das setzt voraus, dass ControlPort im Options-Block aktiviert ist (zusätzliche Thrift-Abhängigkeit). Für das Kivy-Projekt war das bewusst nicht die gewählte Lösung: Der direkte .level()-Zugriff im selben Prozess (Abschnitt 2/3) kommt ohne zusätzliche Server-Infrastruktur aus und war für eine lokale Desktop-App die einfachere Wahl. ControlPort lohnt sich dort, wo mehrere unabhängige Prozesse oder Rechner denselben laufenden Flowgraph beobachten sollen.

8. Function Probe

Ein Sonderfall: kein Signalverarbeitungsblock mit Ein-/Ausgang, sondern ein GRC-Variablenblock. Er pollt in einem eigenen Hintergrundthread periodisch eine beliebige Python-Methode — typischerweise .level() eines der obigen Probe-Blöcke — und schreibt das Ergebnis in eine ganz normale GRC-Variable.

Function Probe Block

Parameter Bedeutung
block_id (Block ID) ID des Blocks, dessen Methode aufgerufen werden soll (leer = die Flowgraph-Klasse selbst)
function_name (Function Name) Name der aufzurufenden Methode, z. B. level
function_args (Function Args) Argumente für den Aufruf, meist leer
poll_rate (Poll Rate (Hz)) wie oft pro Sekunde der Wert abgefragt wird
value (Initial Value) Startwert der Variable, bevor der erste Poll durchgelaufen ist

Inhalt der Variable: exakt das, was die aufgerufene Methode zurückgibt — bei einem Probe-Signal-Block also ein float, bei einem Probe-Signal-Vector-Block eine Liste. Der generierte Code startet dafür einen threading.Thread, der in einer Endlosschleife self.<block_id>.<function_name>(<function_args>) aufruft, das Ergebnis der Variable zuweist und danach 1.0 / poll_rate Sekunden schläft.

Bereits verifiziert im [Frontpanel-Artikel](/projekte/ssb-usb-frontpanel/#2-live-werte-in-die-anzeige-bekommen), Abschnitt 2: Dort ruft ein Function Probe zehnmal pro Sekunde `.level()` eines Probe-Avg-Mag^2-Blocks ab und schreibt das Ergebnis in eine Variable, die wiederum als `value` eines `QT GUI Dial Gauge` (S-Meter) verwendet wird — tatsächlich kompiliert und ausgeführt.