GNU Radio Companion
Test-Beispiel — BCD-Decoder von DCF77
Ein vollständiges, lauffähiges Testbeispiel nach der in "Testen mit GRC" beschriebenen Methodik — angewendet auf den echten BCD-Decoder aus dem DCF77-Projekt.
Dieses Dokument wendet die in Testen mit GRC beschriebene Methodik (Ground Truth getrennt vom Testsignal, feste Testfälle statt Zufall, assert-basierte Prüfung) auf einen konkreten, im Projekt bereits existierenden Baustein an: den BCD-Decoder aus DCF77-Signal demodulieren.
sync_block) — vector_source/top_block aus Testen mit GRC werden hier deshalb nicht gebraucht. Die Testprinzipien (Ground Truth separat führen, feste statt zufälliger Eingaben, Assertions statt Sichtprüfung) gelten aber unverändert.dcf77_decoder.py, test_dcf77_decoder.py, requirements.txt) fertig lauffähig als ZIP —
dcf77-bcd-test.zip herunterladen, entpacken, dann:
pip install -r requirements.txt # installiert pytest
pytest test_dcf77_decoder.py -v
pip install -r requirements.txt # installiert pytest
pytest test_dcf77_decoder.py -v
Ohne pytest funktioniert auch der direkte Aufruf python3 test_dcf77_decoder.py — dafür ist keine zusätzliche Installation nötig.
Der zu testende Code
Zur Erinnerung der Ausschnitt aus DCF77-Signal demodulieren, der hier getestet wird — bcd_decode() als Kernbaustein und decode_dcf77() als Funktion, die alle Felder eines DCF77-Zeitrahmens zusammensetzt:
def decode_dcf77(bits):
if len(bits) < 59:
return f"Unvollständig ({len(bits)} Bits)"
def bcd_decode(bit_slice, weights):
return sum(bit * w for bit, w in zip(bit_slice, weights))
try:
mins = bcd_decode(bits[21:28], [1, 2, 4, 8, 10, 20, 40])
hours = bcd_decode(bits[29:35], [1, 2, 4, 8, 10, 20])
day = bcd_decode(bits[36:42], [1, 2, 4, 8, 10, 20])
weekday_num = bcd_decode(bits[42:45], [1, 2, 4])
month = bcd_decode(bits[45:50], [1, 2, 4, 8, 10])
year = bcd_decode(bits[50:58], [1, 2, 4, 8, 10, 20, 40, 80])
days_map = ["?", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
weekday = days_map[weekday_num] if 1 <= weekday_num <= 7 else "?"
return f"{weekday}, {day:02d}.{month:02d}.20{year:02d} - {hours:02d}:{mins:02d} Uhr"
except Exception as e:
return f"Dekodierfehler: {e}"
def decode_dcf77(bits):
if len(bits) < 59:
return f"Unvollständig ({len(bits)} Bits)"
def bcd_decode(bit_slice, weights):
return sum(bit * w for bit, w in zip(bit_slice, weights))
try:
mins = bcd_decode(bits[21:28], [1, 2, 4, 8, 10, 20, 40])
hours = bcd_decode(bits[29:35], [1, 2, 4, 8, 10, 20])
day = bcd_decode(bits[36:42], [1, 2, 4, 8, 10, 20])
weekday_num = bcd_decode(bits[42:45], [1, 2, 4])
month = bcd_decode(bits[45:50], [1, 2, 4, 8, 10])
year = bcd_decode(bits[50:58], [1, 2, 4, 8, 10, 20, 40, 80])
days_map = ["?", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
weekday = days_map[weekday_num] if 1 <= weekday_num <= 7 else "?"
return f"{weekday}, {day:02d}.{month:02d}.20{year:02d} - {hours:02d}:{mins:02d} Uhr"
except Exception as e:
return f"Dekodierfehler: {e}"
Für den Test wird angenommen, dass dieser Code in einer eigenen Datei dcf77_decoder.py liegt (siehe Checkliste in Testen mit GRC: eigene Blöcke/Funktionen immer als importierbare Datei halten, nicht nur im Artikel- oder GRC-Textfeld).
Ground Truth: eine verifizierte Bitfolge
Als Testfall dient die im Artikel bereits dokumentierte, reale 59-Bit-Sequenz samt erwartetem Ergebnis. Die Ground Truth wird bewusst nicht mit bcd_decode selbst erzeugt, sondern von Hand aus der im Artikel gezeigten Bitpositions-Tabelle abgelesen:
# Aus DCF77-Signal demodulieren, Abschnitt "Bit-Sequenz"
BITFOLGE = "01101100111000100010100011101001010000010000110000011001000"
ERWARTETES_ERGEBNIS = "Do, 08.01.2026 - 14:38 Uhr"
# Bitpositions-Tabelle aus demselben Artikel, als unabhaengige Kontrolle
# der Einzelfelder (nicht nur des Gesamtergebnisses):
ERWARTETE_FELDER = {
"minuten": (21, 28, [1, 2, 4, 8, 10, 20, 40], 38),
"stunden": (29, 35, [1, 2, 4, 8, 10, 20], 14),
"tag": (36, 42, [1, 2, 4, 8, 10, 20], 8),
"monat": (45, 50, [1, 2, 4, 8, 10], 1),
"jahr": (50, 58, [1, 2, 4, 8, 10, 20, 40, 80], 26),
}
# Aus DCF77-Signal demodulieren, Abschnitt "Bit-Sequenz"
BITFOLGE = "01101100111000100010100011101001010000010000110000011001000"
ERWARTETES_ERGEBNIS = "Do, 08.01.2026 - 14:38 Uhr"
# Bitpositions-Tabelle aus demselben Artikel, als unabhaengige Kontrolle
# der Einzelfelder (nicht nur des Gesamtergebnisses):
ERWARTETE_FELDER = {
"minuten": (21, 28, [1, 2, 4, 8, 10, 20, 40], 38),
"stunden": (29, 35, [1, 2, 4, 8, 10, 20], 14),
"tag": (36, 42, [1, 2, 4, 8, 10, 20], 8),
"monat": (45, 50, [1, 2, 4, 8, 10], 1),
"jahr": (50, 58, [1, 2, 4, 8, 10, 20, 40, 80], 26),
}
Die Tests
import pytest
from dcf77_decoder import decode_dcf77
def bits_from_string(s):
"""Wandelt einen Bitstring ('0101...') in eine Liste von int (0/1) um,
wie sie decode_dcf77() erwartet."""
return [int(b) for b in s]
def test_bcd_decode_einzelfelder():
"""Prueft jedes Zeitfeld einzeln gegen die von Hand aus der
Bitpositions-Tabelle abgelesene Ground Truth."""
bits = bits_from_string(BITFOLGE)
def bcd_decode(bit_slice, weights):
return sum(bit * w for bit, w in zip(bit_slice, weights))
for name, (start, end, weights, erwartet) in ERWARTETE_FELDER.items():
ergebnis = bcd_decode(bits[start:end], weights)
assert ergebnis == erwartet, f"{name}: erwartet {erwartet}, erhalten {ergebnis}"
def test_decode_dcf77_vollstaendiger_rahmen():
"""Prueft das zusammengesetzte Endergebnis gegen die dokumentierte Ausgabe."""
bits = bits_from_string(BITFOLGE)
ergebnis = decode_dcf77(bits)
assert ergebnis == ERWARTETES_ERGEBNIS
def test_decode_dcf77_zu_kurze_bitfolge():
"""Ein unvollstaendiger Rahmen (< 59 Bit) muss klar als solcher
erkannt werden, statt eine falsche Zeit vorzutaeuschen."""
kurze_bits = bits_from_string(BITFOLGE)[:40]
ergebnis = decode_dcf77(kurze_bits)
assert ergebnis == "Unvollständig (40 Bits)"
def test_decode_dcf77_ungueltiger_wochentag():
"""Kippt ein Bit im Wochentag-Feld (bits[42:45]) so, dass die BCD-Summe
ausserhalb des gueltigen Bereichs 1..7 liegt - der Decoder muss dann
'?' statt eines falschen Wochentagsnamens liefern, statt z.B. mit
IndexError abzustuerzen."""
bits = bits_from_string(BITFOLGE)
bits[42], bits[43], bits[44] = 0, 0, 0 # weekday_num = 0 -> ausserhalb 1..7
ergebnis = decode_dcf77(bits)
assert ergebnis.startswith("?,")
if __name__ == "__main__":
# Ohne pytest direkt ausfuehrbar - fuer schnelles manuelles Nachvollziehen
BITFOLGE = "01101100111000100010100011101001010000010000110000011001000"
ERWARTETES_ERGEBNIS = "Do, 08.01.2026 - 14:38 Uhr"
ERWARTETE_FELDER = {
"minuten": (21, 28, [1, 2, 4, 8, 10, 20, 40], 38),
"stunden": (29, 35, [1, 2, 4, 8, 10, 20], 14),
"tag": (36, 42, [1, 2, 4, 8, 10, 20], 8),
"monat": (45, 50, [1, 2, 4, 8, 10], 1),
"jahr": (50, 58, [1, 2, 4, 8, 10, 20, 40, 80], 26),
}
test_bcd_decode_einzelfelder()
test_decode_dcf77_vollstaendiger_rahmen()
test_decode_dcf77_zu_kurze_bitfolge()
test_decode_dcf77_ungueltiger_wochentag()
print("Alle Tests erfolgreich.")
import pytest
from dcf77_decoder import decode_dcf77
def bits_from_string(s):
"""Wandelt einen Bitstring ('0101...') in eine Liste von int (0/1) um,
wie sie decode_dcf77() erwartet."""
return [int(b) for b in s]
def test_bcd_decode_einzelfelder():
"""Prueft jedes Zeitfeld einzeln gegen die von Hand aus der
Bitpositions-Tabelle abgelesene Ground Truth."""
bits = bits_from_string(BITFOLGE)
def bcd_decode(bit_slice, weights):
return sum(bit * w for bit, w in zip(bit_slice, weights))
for name, (start, end, weights, erwartet) in ERWARTETE_FELDER.items():
ergebnis = bcd_decode(bits[start:end], weights)
assert ergebnis == erwartet, f"{name}: erwartet {erwartet}, erhalten {ergebnis}"
def test_decode_dcf77_vollstaendiger_rahmen():
"""Prueft das zusammengesetzte Endergebnis gegen die dokumentierte Ausgabe."""
bits = bits_from_string(BITFOLGE)
ergebnis = decode_dcf77(bits)
assert ergebnis == ERWARTETES_ERGEBNIS
def test_decode_dcf77_zu_kurze_bitfolge():
"""Ein unvollstaendiger Rahmen (< 59 Bit) muss klar als solcher
erkannt werden, statt eine falsche Zeit vorzutaeuschen."""
kurze_bits = bits_from_string(BITFOLGE)[:40]
ergebnis = decode_dcf77(kurze_bits)
assert ergebnis == "Unvollständig (40 Bits)"
def test_decode_dcf77_ungueltiger_wochentag():
"""Kippt ein Bit im Wochentag-Feld (bits[42:45]) so, dass die BCD-Summe
ausserhalb des gueltigen Bereichs 1..7 liegt - der Decoder muss dann
'?' statt eines falschen Wochentagsnamens liefern, statt z.B. mit
IndexError abzustuerzen."""
bits = bits_from_string(BITFOLGE)
bits[42], bits[43], bits[44] = 0, 0, 0 # weekday_num = 0 -> ausserhalb 1..7
ergebnis = decode_dcf77(bits)
assert ergebnis.startswith("?,")
if __name__ == "__main__":
# Ohne pytest direkt ausfuehrbar - fuer schnelles manuelles Nachvollziehen
BITFOLGE = "01101100111000100010100011101001010000010000110000011001000"
ERWARTETES_ERGEBNIS = "Do, 08.01.2026 - 14:38 Uhr"
ERWARTETE_FELDER = {
"minuten": (21, 28, [1, 2, 4, 8, 10, 20, 40], 38),
"stunden": (29, 35, [1, 2, 4, 8, 10, 20], 14),
"tag": (36, 42, [1, 2, 4, 8, 10, 20], 8),
"monat": (45, 50, [1, 2, 4, 8, 10], 1),
"jahr": (50, 58, [1, 2, 4, 8, 10, 20, 40, 80], 26),
}
test_bcd_decode_einzelfelder()
test_decode_dcf77_vollstaendiger_rahmen()
test_decode_dcf77_zu_kurze_bitfolge()
test_decode_dcf77_ungueltiger_wochentag()
print("Alle Tests erfolgreich.")
Ausführung mit pytest:
pytest test_dcf77_decoder.py -v
pytest test_dcf77_decoder.py -v
Was diese Tests abdecken — und was nicht
decode_dcf77() aktuell gar nicht ausgewertet — ein Test dafür würde also nur eine Lücke im Code aufdecken, keine vorhandene Funktion prüfen. Ebenso ungetestet: Schaltjahr-Sonderfälle und der Rufbit-Mechanismus (Katastrophenwarnung), die laut Artikel in den ersten 15, verschlüsselten Bits stecken und nicht Teil dieser Decode-Funktion sind.