#!/usr/bin/env python3
"""Baut cw_live.grc programmatisch - dieselbe Verarbeitungskette wie cw.grc,
aber mit einem Front-End fuer echte KiwiSDR-IQ-Aufnahmen (stereo WAV, I/Q
statt Mono-Audio nach einem BFO). Reuses die identischen epy_*.py-Bloecke.
"""
import yaml


def read(path):
    with open(path) as f:
        return f.read()


def block(name, id_, params, states_extra=None, io_cache=None):
    states = {
        "bus_sink": False, "bus_source": False, "bus_structure": None,
        "coordinate": [0, 0], "rotation": 0, "state": "enabled",
    }
    if states_extra:
        states.update(states_extra)
    if io_cache:
        states["_io_cache"] = io_cache
    return {"name": name, "id": id_, "parameters": params, "states": states}


def main():
    nf_src = read("epy_noise_floor.py")
    tagger_src = read("epy_element_tagger.py")
    decoder_src = read("epy_decoder.py")
    afc_src = read("epy_afc.py")

    doc = {
        "options": {
            "parameters": {
                "author": "berndewert", "catch_exceptions": "True",
                "category": "[GRC Hier Blocks]", "cmake_opt": "", "comment": "",
                "description": "CW-Demodulator fuer echte KiwiSDR-IQ-Aufnahmen (Stufe 3)",
                "gen_cmake": "On", "gen_linking": "dynamic",
                "generate_options": "qt_gui", "hier_block_src_path": ".:",
                "id": "cw_live", "max_nouts": "0", "output_language": "python",
                "placement": "(0,0)", "qt_qss_theme": "", "realtime_scheduling": "",
                "run": "True", "run_command": "{python} -u {filename}",
                "run_options": "prompt", "sizing_mode": "fixed",
                "thread_safe_setters": "", "title": "CW-Demodulator (Live-IQ)",
                "window_size": "(1000,1000)",
            },
            "states": {"bus_sink": False, "bus_source": False, "bus_structure": None,
                        "coordinate": [8, 8], "rotation": 0, "state": "enabled"},
        },
        "blocks": [
            block("samp_rate", "variable", {"comment": "aus der KiwiSDR-Aufnahme (12 kS/s IQ)", "value": "12000"},
                  {"coordinate": [184, 12]}),
            block("wpm", "variable",
                  {"comment": "Anfangsschaetzung, per Spektralanalyse der Aufnahme grob bestimmt",
                   "value": "18"}, {"coordinate": [328, 12]}),
            block("pitch", "variable",
                  {"comment": "Frequenzversatz des Zielsignals relativ zur Aufnahme-Mittenfrequenz "
                              "(7033.98 kHz) - per FFT-Peak-Suche ermittelt, nicht die HF selbst",
                   "value": "4510.6"}, {"coordinate": [456, 12]}),
            block("cw_bw", "variable",
                  {"comment": "wie in cw.grc: Nutzbandbreite + AFC-Fangbereich", "value": "max(4.0*wpm, 100.0) + 80.0"},
                  {"coordinate": [584, 12]}),
            block("decim", "variable",
                  {"comment": "12000/12=1000 Sa/s working_rate - identisch zu cw.grc, damit dieselben "
                              "Threshold-/mu-Werte gueltig bleiben", "value": "12"},
                  {"coordinate": [712, 12]}),
            block("working_rate", "variable", {"comment": "", "value": "samp_rate/decim"},
                  {"coordinate": [840, 12]}),

            block("blocks_wavfile_source_0", "blocks_wavfile_source", {
                "affinity": "", "alias": "", "comment":
                    "Stereo-WAV: Kanal 0 = I, Kanal 1 = Q (KiwiSDR-IQ-Konvention)",
                "file": "SND.kiwisdr.dynpc.net_2026-08-05T07_34_42Z_7033.98_iq.wav",
                "maxoutbuf": "0", "minoutbuf": "0", "nchan": "2", "repeat": "False",
            }, {"coordinate": [16, 156]}),

            block("blocks_float_to_complex_0", "blocks_float_to_complex", {
                "affinity": "", "alias": "", "comment":
                    "kombiniert die zwei realen WAV-Kanaele zum komplexen IQ-Signal",
                "maxoutbuf": "0", "minoutbuf": "0", "vlen": "1",
            }, {"coordinate": [232, 156]}),

            block("blocks_throttle_0", "blocks_throttle", {
                "affinity": "", "alias": "", "comment": "", "ignoretag": "True",
                "maxoutbuf": "0", "minoutbuf": "0", "samples_per_second": "samp_rate",
                "type": "complex", "vlen": "1",
            }, {"coordinate": [392, 156]}),

            block("freq_xlating_fir_filter_xxx_0", "freq_xlating_fir_filter_xxx", {
                "affinity": "", "alias": "cw_xlate", "comment":
                    "Complex->Complex (Real Taps): Eingang ist bereits komplexes IQ, "
                    "nicht reelles Audio wie in cw.grc - deshalb Typ 'ccf' statt 'fcf'.",
                "center_freq": "pitch", "decim": "decim", "maxoutbuf": "0", "minoutbuf": "0",
                "samp_rate": "samp_rate", "type": "ccf",
                "taps": "firdes.low_pass(1.0, samp_rate, cw_bw/2, cw_bw/2, window.WIN_HAMMING)",
            }, {"coordinate": [552, 148]}),

            block("blocks_complex_to_mag_0", "blocks_complex_to_mag", {
                "affinity": "", "alias": "", "comment": "", "maxoutbuf": "0", "minoutbuf": "0", "vlen": "1",
            }, {"coordinate": [752, 172]}),

            block("epy_block_0", "epy_block", {
                "_source_code": nf_src, "affinity": "", "alias": "cw_noise_floor", "comment": "",
                "maxoutbuf": "0", "minoutbuf": "0", "num_bins": "20", "ref_wpm": "30.0",
                "samp_rate": "working_rate",
            }, {"coordinate": [904, 172]},
                io_cache="('CW Noise-Floor Normalizer', 'blk', [('samp_rate', '1000.0'), "
                         "('ref_wpm', '30.0'), ('num_bins', '20')], [('0', 'float', 1)], "
                         "[('0', 'float', 1)], '', ['num_bins', 'ref_wpm', 'samp_rate'])"),

            block("blocks_threshold_ff_0", "blocks_threshold_ff", {
                "affinity": "", "alias": "", "comment": "", "high": "6.0", "init": "0", "low": "3.0",
                "maxoutbuf": "0", "minoutbuf": "0",
            }, {"coordinate": [1112, 204]}),

            block("epy_block_1", "epy_block", {
                "_source_code": tagger_src, "affinity": "", "alias": "cw_element_tagger",
                "comment": "", "mu": "0.7", "maxoutbuf": "0", "minoutbuf": "0",
                "samp_rate": "working_rate", "wpm": "wpm",
            }, {"coordinate": [1304, 204]},
                io_cache="('CW Element Tagger', 'blk', [('samp_rate', '1000.0'), ('wpm', '20.0'), "
                         "('mu', '0.7')], [('0', 'float', 1)], [('0', 'float', 1)], '', "
                         "['mu', 'samp_rate', 'wpm'])"),

            block("epy_block_2", "epy_block", {
                "_source_code": decoder_src, "affinity": "", "alias": "cw_decoder", "comment": "",
                "maxoutbuf": "0", "minoutbuf": "0",
            }, {"coordinate": [1512, 276]},
                io_cache="('CW Decoder', 'blk', [], [('0', 'float', 1)], "
                         "[('out', 'message', 1)], '', [])"),

            block("epy_block_3", "epy_block", {
                "_source_code": afc_src, "affinity": "", "alias": "cw_afc", "comment": "",
                "guard_samples": "15", "loop_gain": "0.05", "maxoutbuf": "0", "minoutbuf": "0",
                "pitch": "pitch", "samp_rate": "working_rate",
            }, {"coordinate": [752, 340]},
                io_cache="('CW AFC', 'blk', [('samp_rate', '1000.0'), ('pitch', '700.0'), "
                         "('loop_gain', '0.05'), ('guard_samples', '15')], "
                         "[('0', 'complex', 1)], [('freq', 'message', 1)], '', "
                         "['guard_samples', 'loop_gain', 'pitch', 'samp_rate'])"),

            block("blocks_message_debug_0", "blocks_message_debug", {
                "affinity": "", "alias": "", "comment": "", "en_uvec": "True",
            }, {"coordinate": [1720, 204]}),
        ],
        "connections": [
            ["blocks_wavfile_source_0", "0", "blocks_float_to_complex_0", "0"],
            ["blocks_wavfile_source_0", "1", "blocks_float_to_complex_0", "1"],
            ["blocks_float_to_complex_0", "0", "blocks_throttle_0", "0"],
            ["blocks_throttle_0", "0", "freq_xlating_fir_filter_xxx_0", "0"],
            ["freq_xlating_fir_filter_xxx_0", "0", "blocks_complex_to_mag_0", "0"],
            ["freq_xlating_fir_filter_xxx_0", "0", "epy_block_3", "0"],
            ["blocks_complex_to_mag_0", "0", "epy_block_0", "0"],
            ["epy_block_0", "0", "blocks_threshold_ff_0", "0"],
            ["blocks_threshold_ff_0", "0", "epy_block_1", "0"],
            ["epy_block_1", "0", "epy_block_2", "0"],
            ["epy_block_2", "out", "blocks_message_debug_0", "print"],
            ["epy_block_3", "freq", "freq_xlating_fir_filter_xxx_0", "freq"],
        ],
        "metadata": {"file_format": 1, "grc_version": "3.10.12.0"},
    }

    with open("cw_live.grc", "w") as f:
        yaml.dump(doc, f, sort_keys=False, allow_unicode=True, width=100)
    print("Geschrieben: cw_live.grc")


if __name__ == "__main__":
    main()
