buffered_probe

Eines der Block-Tests-Unterverzeichnisse (siehe ../README.md
für die Übersicht). Ausgangspunkt war die Frage, ob (und wie) sich ein
eigener Probe-Block bauen lässt, der Daten bis zur Abholung puffert,
statt wie Probe Signal Vector nur den zuletzt empfangenen Block zu behalten
oder wie Vector Sink unbegrenzt und ohne dokumentierte Thread-Sicherheit zu
wachsen.

buffered_probe

Ein eigener gr.sync_block (reines Python, keine externe Bibliothek außer
NumPy). Sammelt alle eingehenden Float-Samples in einem
collections.deque(maxlen=...), abgesichert durch ein eigenes
threading.Lock. Zwei Methoden nach außen:

Anders als bei Vector Sink ist die Thread-Sicherheit hier explizit selbst
gebaut und getestet
(siehe unten), nicht implizit vorausgesetzt.

Dateien

Tests

python3 test_buffered_probe.py
python3 test_buffered_probe.py
Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt):
=== 1. Luecken-Test ===
gesammelte Samples: 8192, Luecken: 0
OK - keine Luecken

=== 2. Nebenlaeufigkeits-Stresstest ===
Poll-Thread-Fehler: 0
total_in_count(): 196596, tatsaechlich abgeholt: 196596
Luecken trotz Dauerbelastung: 0
OK - keine Exceptions, keine Datenverluste, keine Luecken unter Last

=== 3. Overflow-Test (max_len-Grenze) ===
buffered_count() nach Ueberlastung: 1000 (max_len=1000)
OK - Puffer korrekt auf max_len begrenzt
=== 1. Luecken-Test ===
gesammelte Samples: 8192, Luecken: 0
OK - keine Luecken

=== 2. Nebenlaeufigkeits-Stresstest ===
Poll-Thread-Fehler: 0
total_in_count(): 196596, tatsaechlich abgeholt: 196596
Luecken trotz Dauerbelastung: 0
OK - keine Exceptions, keine Datenverluste, keine Luecken unter Last

=== 3. Overflow-Test (max_len-Grenze) ===
buffered_count() nach Ueberlastung: 1000 (max_len=1000)
OK - Puffer korrekt auf max_len begrenzt

Test 2 ist der entscheidende: Ein separater Python-Thread ruft get_and_clear()
ohne Pause auf, während GNU Radio parallel im Scheduler-Thread work()
aufruft — bewusst so aggressiv wie möglich, um mit dem Lock zu konkurrieren.
Über ~197.000 Samples hinweg: keine Exception, keine verlorenen oder
duplizierten Samples, keine Lücken. Das ist genau die Garantie, die bei
blocks.vector_sink_x nicht offiziell dokumentiert ist (siehe
SignalAnalyzer-Diskussion).

Signaltreue-Test mit PNG-Ausgabe

python3 test_buffered_probe_signal.py
python3 test_buffered_probe_signal.py
Verifiziert (GNU Radio 3.10.12.0, tatsächlich ausgeführt) — ein Chirp-Testsignal (300 Hz → 3000 Hz, 2 s bei 48 kHz) läuft durch `Throttle` + `buffered_probe`, wird mit 20 Hz gepollt (wie `Clock.schedule_interval` im Kivy-Frontpanel-Projekt) und aus allen `get_and_clear()`-Häppchen wieder zusammengesetzt:
=== Signaltreue ===
verglichene Samples: 96000
RMS-Fehler: 0.00e+00
Maximaler Fehler: 0.00e+00
(Werte im Bereich der float32-Rechengenauigkeit = verlust- und lückenlos)
=== Signaltreue ===
verglichene Samples: 96000
RMS-Fehler: 0.00e+00
Maximaler Fehler: 0.00e+00
(Werte im Bereich der float32-Rechengenauigkeit = verlust- und lückenlos)

Original- und rekonstruiertes Chirp-Signal liegen deckungsgleich uebereinander, Fehlerkurve konstant bei Null

Auffälligkeit bei den Poll-Größen: Erwartet wurden bei 48 kHz und 20 Hz-Polling im Schnitt ca. 2400 Samples pro Abfrage — tatsächlich liefern die meisten Polls **0** Samples, dafür alle paar Zyklen ein Schub von genau 8191–8192 Samples. Das liegt nicht an buffered_probe, sondern an GNU Radios interner Standard-Puffergröße (8192 Items): Throttle gibt Daten nicht gleichmäßig Sample für Sample weiter, sondern in Schüben, sobald ein interner Puffer voll ist. Separat mit einer einfachen Poll-Schleife nachgemessen: 20 Abfragen im 50-ms-Takt, Ergebnis abwechselnd 0, 0, 8192, 0, 0, 8191, ... — ein Muster, das sich exakt mit 96000 Samples ÷ 8192 ≈ 11,7 Schüben deckt. Für buffered_probe ist das unerheblich (es puffert ja gerade für diesen Fall), aber wichtig für die Erwartungshaltung, wie granular GNU-Radio-Daten tatsächlich beim Poller ankommen.

GRC-Einbindung verifiziert

grcc buffered_probe_demo.grc -o .
python3 -c "
from buffered_probe_demo import buffered_probe_demo
tb = buffered_probe_demo()
tb.start()
import time; time.sleep(0.5)
print(len(tb.epy_block_0.get_and_clear()), 'Samples abgeholt')
tb.stop(); tb.wait()
"
grcc buffered_probe_demo.grc -o .
python3 -c "
from buffered_probe_demo import buffered_probe_demo
tb = buffered_probe_demo()
tb.start()
import time; time.sleep(0.5)
print(len(tb.epy_block_0.get_and_clear()), 'Samples abgeholt')
tb.stop(); tb.wait()
"

Ergebnis: 8192 echte Kosinus-Testtonsamples abgeholt — der unveränderte
buffered_probe.py-Code läuft identisch in GRC wie im Unit-Test.

Kleine, real aufgetretene GRC-Falle beim Bau von buffered_probe_demo.grc: Wird die Klasse im Embedded-Python-Block-Quellcode per from buffered_probe import buffered_probe as blk umbenannt importiert, generiert grcc trotzdem einen Aufruf der Form epy_block_0.buffered_probe(...) statt epy_block_0.blk(...) — GRC übernimmt offenbar den tatsächlichen Klassennamen (__name__), nicht den lokalen Alias. Lösung: ohne as blk importieren, dann stimmen Klassenname und generierter Aufruf überein.

Vergleich mit den eingebauten Alternativen

Probe Signal Vector Vector Sink buffered_probe (eigen)
Verhalten behält nur den letzten Block akkumuliert alles, unbegrenzt akkumuliert alles, bis max_len
Thread-Sicherheit offiziell dokumentiert nicht dokumentiert selbst gebaut, per Stresstest verifiziert
Speicherwachstum konstant (fester vlen) unbegrenzt ohne manuelles reset() begrenzt, älteste Samples fallen bei Überlauf weg
Geeignet für Live-Anzeige (aktueller Schnappschuss) kurze, kontrollierte Aufzeichnungen Auswertung über mehrere Blöcke hinweg, ohne Daten zu verlieren

Nächste Schritte

Weitere eigene Blöcke, die sich nach demselben Muster (Implementierung +
Unit-Test + GRC-Demo-Flowgraph) hier ergänzen ließen — Platz für zukünftige
Experimente jenseits von buffered_probe.