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.

Der Decoder in Stufe 1 ist eine reine Python-Funktion (kein GNU-Radio-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.
Download: Alle drei Dateien (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

Abgedeckt: Die BCD-Arithmetik für jedes einzelne Zeitfeld (unabhängig voneinander geprüft, nicht nur über das zusammengesetzte Endergebnis), das Fehlerverhalten bei unvollständigen Rahmen, und der Grenzfall eines ungültigen Wochentagswerts. Alle vier Tests basieren auf einer einzigen, real verifizierten Bitfolge aus dem Projekt selbst — keine erfundenen Zahlen.
Nicht abgedeckt: Die Paritätsbits (P1/P2/P3 aus der Bitpositions-Tabelle) werden von 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.