#!/usr/bin/env python3
"""
SDR TV & RF Analysis Tool

Multi-mode SDR application for RTL-SDR V4:
  - DVB-T digital TV reception (MPEG-TS output)
  - Analog TV reception (PAL/NTSC via FM demod)
  - Spectrum analyzer (terminal-based power display)
  - Wireless camera scanner (common security camera frequencies)

Designed for authorized RF security testing and research.

Usage:
    python3 sdr_tv.py dvbt --freq 506e6 | mpv -
    python3 sdr_tv.py analog --freq 175.25e6 --standard pal | mpv --demuxer=rawvideo --rawvideo=w=768:h=576:format=gray -
    python3 sdr_tv.py spectrum --freq 500e6 --span 20e6
    python3 sdr_tv.py camera --preset 1.2ghz
"""

import argparse
import signal
import sys
import time
import struct
import threading
import numpy as np

from gnuradio import gr, blocks, fft, dtv, analog, filter
import osmosdr


# =============================================================================
# Constants & Presets
# =============================================================================

# DVB-T parameters
TRANSMISSION_MODE_MAP = {
    '2k': (dtv.T2k, 2048, 1705),
    '8k': (dtv.T8k, 8192, 6817),
}

CONSTELLATION_MAP = {
    'qpsk':  dtv.MOD_QPSK,
    '16qam': dtv.MOD_16QAM,
    '64qam': dtv.MOD_64QAM,
}

CODE_RATE_MAP = {
    '1/2': dtv.C1_2,
    '2/3': dtv.C2_3,
    '3/4': dtv.C3_4,
    '5/6': dtv.C5_6,
    '7/8': dtv.C7_8,
}

GUARD_INTERVAL_MAP = {
    '1/4':  (dtv.GI_1_4, 4),
    '1/8':  (dtv.GI_1_8, 8),
    '1/16': (dtv.GI_1_16, 16),
    '1/32': (dtv.GI_1_32, 32),
}

# Wireless camera frequency presets for authorized security testing
# These are commonly documented frequencies used by analog wireless cameras
CAMERA_PRESETS = {
    '900mhz': {
        'name': '900 MHz ISM Band',
        'channels': [
            (910.0e6, '910 MHz Ch1'),
            (920.0e6, '920 MHz Ch2'),
            (930.0e6, '930 MHz Ch3'),
            (940.0e6, '940 MHz Ch4'),
        ],
        'bandwidth': 6.0e6,
        'modulation': 'fm',
    },
    '1.2ghz': {
        'name': '1.2 GHz Band',
        'channels': [
            (1080.0e6, '1080 MHz Ch1'),
            (1120.0e6, '1120 MHz Ch2'),
            (1160.0e6, '1160 MHz Ch3'),
            (1200.0e6, '1200 MHz Ch4'),
            (1240.0e6, '1240 MHz Ch5'),
            (1280.0e6, '1280 MHz Ch6'),
            (1320.0e6, '1320 MHz Ch7'),
        ],
        'bandwidth': 10.0e6,
        'modulation': 'fm',
    },
    '2.4ghz': {
        'name': '2.4 GHz ISM Band',
        'channels': [
            (2411.0e6, '2411 MHz Ch1'),
            (2431.0e6, '2431 MHz Ch2'),
            (2451.0e6, '2451 MHz Ch3'),
            (2473.0e6, '2473 MHz Ch4'),
        ],
        'bandwidth': 10.0e6,
        'modulation': 'fm',
    },
    'fpv': {
        'name': 'FPV/Drone Video (5.8 GHz — RTL-SDR cannot tune here)',
        'channels': [
            (5740.0e6, '5740 MHz R1'),
            (5760.0e6, '5760 MHz R2'),
            (5780.0e6, '5780 MHz R3'),
            (5800.0e6, '5800 MHz R4'),
            (5820.0e6, '5820 MHz R5'),
            (5840.0e6, '5840 MHz R6'),
            (5860.0e6, '5860 MHz R7'),
            (5880.0e6, '5880 MHz R8'),
        ],
        'bandwidth': 20.0e6,
        'modulation': 'fm',
    },
}


def parse_freq(s):
    """Parse frequency string, supporting scientific notation like 506e6."""
    return int(float(s))


def log(msg):
    """Print to stderr so stdout stays clean for data output."""
    print(msg, file=sys.stderr)


# =============================================================================
# RTL-SDR Source Helper
# =============================================================================

def create_rtl_source(freq, samp_rate, gain=40.0, if_gain=40.0, ppm=0.0):
    """Create and configure an osmosdr RTL-SDR source."""
    source = osmosdr.source(args="numchan=1")
    source.set_sample_rate(samp_rate)
    source.set_center_freq(freq)
    source.set_freq_corr(ppm)
    source.set_gain_mode(False)
    source.set_gain(gain)
    source.set_if_gain(if_gain)
    source.set_bb_gain(0)
    return source


# =============================================================================
# Mode 1: DVB-T Digital TV Receiver
# =============================================================================

class DVBTReceiver(gr.top_block):
    def __init__(self, args):
        gr.top_block.__init__(self, "DVB-T Receiver")

        tx_mode_enum, fft_length, active_carriers = TRANSMISSION_MODE_MAP[args.transmission_mode]
        constellation = CONSTELLATION_MAP[args.constellation]
        code_rate = CODE_RATE_MAP[args.code_rate]
        gi_enum, gi_divisor = GUARD_INTERVAL_MAP[args.guard_interval]
        cp_length = fft_length // gi_divisor

        if args.bandwidth == 8:
            samp_rate = 64e6 / 7.0
        elif args.bandwidth == 7:
            samp_rate = 8e6
        else:
            samp_rate = 48e6 / 7.0

        log(f"=== DVB-T Digital Receiver ===")
        log(f"Frequency:      {args.freq / 1e6:.3f} MHz")
        log(f"Bandwidth:      {args.bandwidth} MHz")
        log(f"Sample rate:    {samp_rate:.0f} Hz")
        log(f"Mode:           {args.transmission_mode} (FFT={fft_length})")
        log(f"Constellation:  {args.constellation}")
        log(f"Code rate:      {args.code_rate}")
        log(f"Guard interval: {args.guard_interval} (CP={cp_length})")

        # Source
        if args.input_file:
            log(f"Source: file '{args.input_file}'")
            self.source = blocks.file_source(gr.sizeof_gr_complex, args.input_file, repeat=False)
        else:
            log(f"Source: RTL-SDR (live)")
            self.source = create_rtl_source(args.freq, samp_rate, args.gain, args.if_gain, args.ppm)
            actual = self.source.get_sample_rate()
            if abs(actual - samp_rate) > 1000:
                log(f"WARNING: Got {actual:.0f} Hz instead of {samp_rate:.0f} Hz")
                log(f"WARNING: RTL-SDR can't sample fast enough for DVB-T.")
                log(f"WARNING: Use --input-file with a pre-recorded IQ file.")

        # OFDM demodulation chain
        self.s2v = blocks.stream_to_vector(gr.sizeof_gr_complex, fft_length)

        self.ofdm_acq = dtv.dvbt_ofdm_sym_acquisition(
            blocks=1, fft_length=fft_length,
            occupied_tones=active_carriers, cp_length=cp_length, snr=100.0
        )

        self.fft_block = fft.fft_vcc(fft_length, True, [], False)

        self.demod_ref = dtv.dvbt_demod_reference_signals(
            gr.sizeof_gr_complex, fft_length, active_carriers,
            constellation, dtv.NH, code_rate, dtv.C1_2,
            gi_enum, tx_mode_enum, 0, 0
        )

        self.v2s = blocks.vector_to_stream(gr.sizeof_gr_complex, active_carriers)

        self.demap = dtv.dvbt_demap(active_carriers, constellation, dtv.NH, tx_mode_enum, 1.0)

        self.bit_deintlv = dtv.dvbt_bit_inner_deinterleaver(
            active_carriers, constellation, dtv.NH, tx_mode_enum
        )

        self.viterbi = dtv.dvbt_viterbi_decoder(constellation, dtv.NH, code_rate, active_carriers)

        self.conv_deintlv = dtv.dvbt_convolutional_deinterleaver(136, 12, 17)

        self.rs_dec = dtv.dvbt_reed_solomon_dec(2, 8, 0x11d, 255, 239, 8, 51, 8)

        self.descramble = dtv.dvbt_energy_descramble(8)

        self.sink = blocks.file_descriptor_sink(gr.sizeof_char, 1)

        # Wire it up
        self.connect(
            self.source, self.s2v, self.ofdm_acq, self.fft_block,
            self.demod_ref, self.v2s, self.demap, self.bit_deintlv,
            self.viterbi, self.conv_deintlv, self.rs_dec,
            self.descramble, self.sink
        )

        log("Flowgraph ready. MPEG-TS output on stdout.")


# =============================================================================
# Mode 2: Analog TV Receiver (PAL/NTSC via Wideband FM Demod)
# =============================================================================

class AnalogTVReceiver(gr.top_block):
    """
    Analog TV receiver using wideband FM demodulation.
    Outputs raw baseband video as unsigned 8-bit samples to stdout.

    Analog TV transmits composite video via AM on the picture carrier,
    with FM audio offset +4.5 MHz (NTSC) or +5.5 MHz (PAL).

    This receiver demodulates the composite video baseband signal.
    """
    def __init__(self, args):
        gr.top_block.__init__(self, "Analog TV Receiver")

        samp_rate = 2.4e6  # RTL-SDR max stable rate
        audio_rate = 48000

        if args.standard == 'pal':
            fm_deviation = 5.5e6
            video_bw = 5.0e6
        else:  # ntsc
            fm_deviation = 4.5e6
            video_bw = 4.2e6

        log(f"=== Analog TV Receiver ({args.standard.upper()}) ===")
        log(f"Frequency:   {args.freq / 1e6:.3f} MHz")
        log(f"Sample rate: {samp_rate:.0f} Hz")
        log(f"Standard:    {args.standard.upper()}")

        # Source
        if args.input_file:
            log(f"Source: file '{args.input_file}'")
            self.source = blocks.file_source(gr.sizeof_gr_complex, args.input_file, repeat=False)
        else:
            log(f"Source: RTL-SDR (live)")
            self.source = create_rtl_source(args.freq, samp_rate, args.gain, args.if_gain, args.ppm)

        # FM demodulator for composite video
        self.fm_demod = analog.wfm_rcv(
            quad_rate=samp_rate,
            audio_decimation=1,
        )

        # Low-pass filter to isolate video baseband
        video_taps = filter.firdes.low_pass(
            1.0, samp_rate, min(video_bw, samp_rate / 2 - 100e3), 100e3
        )
        self.video_lpf = filter.fir_filter_fff(1, video_taps)

        # Scale float [-1,1] to unsigned byte [0,255] for raw video output
        self.scale = blocks.multiply_const_ff(127.0)
        self.offset = blocks.add_const_ff(128.0)
        self.f2c = blocks.float_to_uchar()

        # Output to stdout
        self.sink = blocks.file_descriptor_sink(gr.sizeof_char, 1)

        self.connect(
            self.source, self.fm_demod, self.video_lpf,
            self.scale, self.offset, self.f2c, self.sink
        )

        log("Flowgraph ready. Raw video on stdout.")
        log(f"Play with: python3 sdr_tv.py analog ... | mpv --demuxer=rawvideo --rawvideo=w=720:h=576:format=gray -")


# =============================================================================
# Mode 3: Spectrum Analyzer (Terminal-based)
# =============================================================================

class SpectrumAnalyzer(gr.top_block):
    """
    Real-time terminal spectrum analyzer.
    Captures IQ, computes FFT, and displays power spectrum in the terminal.
    """
    def __init__(self, args):
        gr.top_block.__init__(self, "Spectrum Analyzer")

        self.fft_size = args.fft_size
        self.samp_rate = min(args.span, 2.4e6)  # RTL-SDR limited
        self.center_freq = args.freq
        self.args = args

        log(f"=== Spectrum Analyzer ===")
        log(f"Center:      {args.freq / 1e6:.3f} MHz")
        log(f"Span:        {self.samp_rate / 1e6:.1f} MHz")
        log(f"FFT size:    {self.fft_size}")
        log(f"Bin width:   {self.samp_rate / self.fft_size:.0f} Hz")

        # Source
        if args.input_file:
            self.source = blocks.file_source(gr.sizeof_gr_complex, args.input_file, repeat=True)
        else:
            self.source = create_rtl_source(args.freq, self.samp_rate, args.gain, args.if_gain, args.ppm)

        # FFT -> magnitude squared -> log
        self.s2v = blocks.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
        self.fft_block = fft.fft_vcc(self.fft_size, True,
                                      fft.window.blackmanharris(self.fft_size), True)
        self.mag = blocks.complex_to_mag_squared(self.fft_size)

        # Probe to read FFT data from Python
        self.probe = blocks.probe_signal_vf(self.fft_size)

        self.connect(self.source, self.s2v, self.fft_block, self.mag, self.probe)

    def display_loop(self):
        """Terminal display loop — runs in main thread."""
        cols = 80
        try:
            cols = os.get_terminal_size().columns
        except Exception:
            pass

        log("")
        while True:
            try:
                data = self.probe.level()
                if len(data) == 0:
                    time.sleep(0.1)
                    continue

                power = np.array(data)
                # Convert to dB, avoiding log(0)
                power_db = 10.0 * np.log10(np.maximum(power, 1e-12))

                # Bin down to terminal width
                num_bins = min(cols - 12, self.fft_size)
                if num_bins < self.fft_size:
                    binned = np.mean(power_db.reshape(num_bins, -1), axis=1)
                else:
                    binned = power_db[:num_bins]

                db_min = self.args.db_min
                db_max = self.args.db_max
                height = 20

                # Clear screen and draw
                sys.stderr.write('\033[2J\033[H')

                freq_start = (self.center_freq - self.samp_rate / 2) / 1e6
                freq_end = (self.center_freq + self.samp_rate / 2) / 1e6
                sys.stderr.write(f'  Spectrum: {freq_start:.2f} - {freq_end:.2f} MHz  '
                                 f'(center {self.center_freq/1e6:.3f} MHz)\n')
                sys.stderr.write(f'  Range: {db_min} to {db_max} dB\n\n')

                # Draw waterfall-style rows
                for row in range(height - 1, -1, -1):
                    db_level = db_min + (db_max - db_min) * row / height
                    label = f'{db_level:6.0f}|'
                    line = []
                    for b in range(num_bins):
                        if binned[b] >= db_level:
                            intensity = min(1.0, (binned[b] - db_level) / ((db_max - db_min) / height))
                            if intensity > 0.7:
                                line.append('\033[91m\u2588\033[0m')  # red = strong
                            elif intensity > 0.3:
                                line.append('\033[93m\u2593\033[0m')  # yellow = medium
                            else:
                                line.append('\033[92m\u2591\033[0m')  # green = weak
                        else:
                            line.append(' ')
                    sys.stderr.write(label + ''.join(line) + '\n')

                # Frequency axis
                sys.stderr.write(' ' * 7 + '\u2514' + '\u2500' * num_bins + '\n')
                axis = ' ' * 7
                axis += f'{freq_start:.1f}'
                mid_pos = num_bins // 2 - 4
                axis += ' ' * (mid_pos - len(f'{freq_start:.1f}'))
                axis += f'{self.center_freq/1e6:.1f}'
                end_pos = num_bins - len(f'{freq_end:.1f}') - mid_pos
                axis += ' ' * max(1, end_pos)
                axis += f'{freq_end:.1f}'
                sys.stderr.write(axis + ' MHz\n')

                # Peak info
                peak_bin = np.argmax(binned)
                peak_freq = freq_start + (freq_end - freq_start) * peak_bin / num_bins
                sys.stderr.write(f'\n  Peak: {binned[peak_bin]:.1f} dB @ {peak_freq:.3f} MHz\n')
                sys.stderr.write(f'  [Ctrl+C to quit]\n')

                time.sleep(0.1)

            except (ValueError, RuntimeError):
                time.sleep(0.2)
            except KeyboardInterrupt:
                break


# =============================================================================
# Mode 4: Wireless Camera Scanner
# =============================================================================

class CameraScanner(gr.top_block):
    """
    Scans known wireless camera frequencies and reports signal strength.
    For authorized security testing only.
    """
    def __init__(self, args):
        gr.top_block.__init__(self, "Camera Scanner")

        self.samp_rate = 2.4e6
        self.fft_size = 1024
        self.args = args

        # Start with a dummy frequency, we'll retune
        self.source = create_rtl_source(100e6, self.samp_rate, args.gain, args.if_gain, args.ppm)

        self.s2v = blocks.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
        self.fft_block = fft.fft_vcc(self.fft_size, True,
                                      fft.window.blackmanharris(self.fft_size), True)
        self.mag = blocks.complex_to_mag_squared(self.fft_size)
        self.probe = blocks.probe_signal_vf(self.fft_size)

        self.connect(self.source, self.s2v, self.fft_block, self.mag, self.probe)

    def measure_power(self, freq, dwell=0.5):
        """Tune to frequency and measure average power."""
        self.source.set_center_freq(freq)
        time.sleep(dwell)
        try:
            data = np.array(self.probe.level())
            if len(data) == 0:
                return -100.0
            power = 10.0 * np.log10(np.maximum(np.mean(data), 1e-12))
            return power
        except (ValueError, RuntimeError):
            return -100.0

    def scan_loop(self):
        """Scan all camera presets and display results."""
        presets = self.args.preset
        if presets == 'all':
            scan_presets = list(CAMERA_PRESETS.keys())
        else:
            scan_presets = [presets]

        log("\n=== Wireless Camera Scanner ===")
        log("For authorized security testing only.\n")

        # RTL-SDR V4 tunes roughly 24 MHz - 1766 MHz
        max_freq = 1766e6

        while True:
            try:
                sys.stderr.write('\033[2J\033[H')
                sys.stderr.write('=== Wireless Camera Scanner ===\n')
                sys.stderr.write('Authorized security testing only.\n\n')

                for preset_name in scan_presets:
                    preset = CAMERA_PRESETS[preset_name]
                    sys.stderr.write(f'--- {preset["name"]} ---\n')

                    for freq, label in preset['channels']:
                        if freq > max_freq:
                            sys.stderr.write(f'  {label:25s}  [OUT OF RANGE for RTL-SDR]\n')
                            continue

                        power = self.measure_power(freq, dwell=0.3)

                        # Signal strength bar
                        bar_len = max(0, int((power + 60) / 2))
                        bar = '\u2588' * bar_len

                        if power > -20:
                            color = '\033[91m'  # red = strong signal
                            status = 'STRONG'
                        elif power > -35:
                            color = '\033[93m'  # yellow = moderate
                            status = 'MODERATE'
                        elif power > -50:
                            color = '\033[92m'  # green = weak
                            status = 'WEAK'
                        else:
                            color = '\033[90m'  # gray = noise floor
                            status = 'noise'

                        sys.stderr.write(
                            f'  {label:25s} {power:7.1f} dB  '
                            f'{color}{bar:20s} {status}\033[0m\n'
                        )

                    sys.stderr.write('\n')

                sys.stderr.write('[Scanning... Ctrl+C to quit]\n')
                time.sleep(1.0)

            except KeyboardInterrupt:
                break


# =============================================================================
# Mode 5: Camera Video Receiver (tune to a specific camera freq and demod)
# =============================================================================

class CameraVideoReceiver(gr.top_block):
    """
    Receives analog wireless camera video via wideband FM demodulation.
    Most analog wireless cameras use FM modulated composite video.
    """
    def __init__(self, args):
        gr.top_block.__init__(self, "Camera Video Receiver")

        samp_rate = 2.4e6

        log(f"=== Camera Video Receiver ===")
        log(f"Frequency: {args.freq / 1e6:.3f} MHz")
        log(f"Demodulation: Wideband FM")

        self.source = create_rtl_source(args.freq, samp_rate, args.gain, args.if_gain, args.ppm)

        # Wideband FM demod (analog cameras typically use ~5 MHz deviation)
        self.fm_demod = analog.wfm_rcv(quad_rate=samp_rate, audio_decimation=1)

        # Low-pass to get composite video
        video_taps = filter.firdes.low_pass(1.0, samp_rate, 1.0e6, 200e3)
        self.video_lpf = filter.fir_filter_fff(1, video_taps)

        # Float to byte for output
        self.scale = blocks.multiply_const_ff(127.0)
        self.offset = blocks.add_const_ff(128.0)
        self.f2c = blocks.float_to_uchar()

        self.sink = blocks.file_descriptor_sink(gr.sizeof_char, 1)

        self.connect(self.source, self.fm_demod, self.video_lpf,
                     self.scale, self.offset, self.f2c, self.sink)

        log("Raw composite video on stdout.")
        log("View with: python3 sdr_tv.py camera-rx ... | ffplay -f rawvideo -pix_fmt gray -video_size 720x576 -")


# =============================================================================
# CLI
# =============================================================================

import os

def main():
    parser = argparse.ArgumentParser(
        description='SDR TV & RF Analysis Tool for RTL-SDR V4',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Modes:
  dvbt        DVB-T digital TV receiver (outputs MPEG-TS to stdout)
  analog      Analog TV receiver PAL/NTSC (outputs raw video to stdout)
  spectrum    Real-time terminal spectrum analyzer
  camera      Scan wireless camera frequencies for signals
  camera-rx   Receive/demodulate a wireless camera signal

Examples:
  python3 sdr_tv.py dvbt --freq 506e6 | mpv -
  python3 sdr_tv.py analog --freq 175.25e6 | ffplay -f rawvideo -pix_fmt gray -video_size 720x576 -
  python3 sdr_tv.py spectrum --freq 500e6
  python3 sdr_tv.py camera --preset all
  python3 sdr_tv.py camera --preset 1.2ghz
  python3 sdr_tv.py camera-rx --freq 1200e6 | ffplay -f rawvideo -pix_fmt gray -video_size 720x576 -
"""
    )

    subparsers = parser.add_subparsers(dest='mode', required=True)

    # Common arguments for all modes
    def add_common_args(p):
        p.add_argument('--gain', type=float, default=40.0, help='RF gain dB (default: 40)')
        p.add_argument('--if-gain', type=float, default=40.0, help='IF gain dB (default: 40)')
        p.add_argument('--ppm', type=float, default=0.0, help='Freq correction PPM (default: 0)')
        p.add_argument('--input-file', type=str, default=None, help='IQ file instead of live SDR')

    # --- DVB-T ---
    p_dvbt = subparsers.add_parser('dvbt', help='DVB-T digital TV receiver')
    p_dvbt.add_argument('--freq', type=parse_freq, required=True, help='Center freq Hz')
    p_dvbt.add_argument('--bandwidth', type=int, default=8, choices=[6, 7, 8], help='BW MHz')
    p_dvbt.add_argument('--transmission-mode', default='8k', choices=['2k', '8k'])
    p_dvbt.add_argument('--constellation', default='64qam', choices=['qpsk', '16qam', '64qam'])
    p_dvbt.add_argument('--code-rate', default='2/3', choices=['1/2', '2/3', '3/4', '5/6', '7/8'])
    p_dvbt.add_argument('--guard-interval', default='1/32', choices=['1/4', '1/8', '1/16', '1/32'])
    add_common_args(p_dvbt)

    # --- Analog TV ---
    p_analog = subparsers.add_parser('analog', help='Analog TV receiver (PAL/NTSC)')
    p_analog.add_argument('--freq', type=parse_freq, required=True, help='Picture carrier freq Hz')
    p_analog.add_argument('--standard', default='pal', choices=['pal', 'ntsc'], help='TV standard')
    add_common_args(p_analog)

    # --- Spectrum Analyzer ---
    p_spec = subparsers.add_parser('spectrum', help='Terminal spectrum analyzer')
    p_spec.add_argument('--freq', type=parse_freq, required=True, help='Center freq Hz')
    p_spec.add_argument('--span', type=float, default=2.4e6, help='Span Hz (max 2.4M for RTL-SDR)')
    p_spec.add_argument('--fft-size', type=int, default=1024, help='FFT size (default: 1024)')
    p_spec.add_argument('--db-min', type=float, default=-60.0, help='Min dB for display')
    p_spec.add_argument('--db-max', type=float, default=0.0, help='Max dB for display')
    add_common_args(p_spec)

    # --- Camera Scanner ---
    p_cam = subparsers.add_parser('camera', help='Scan wireless camera frequencies')
    p_cam.add_argument('--preset', default='all',
                       choices=list(CAMERA_PRESETS.keys()) + ['all'],
                       help='Camera freq preset to scan')
    add_common_args(p_cam)

    # --- Camera Video Receiver ---
    p_camrx = subparsers.add_parser('camera-rx', help='Receive wireless camera video')
    p_camrx.add_argument('--freq', type=parse_freq, required=True, help='Camera freq Hz')
    add_common_args(p_camrx)

    args = parser.parse_args()

    # Signal handling
    def signal_handler(sig, frame):
        log("\nStopping...")
        sys.exit(0)

    signal.signal(signal.SIGINT, signal_handler)
    try:
        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    except AttributeError:
        pass

    # Dispatch
    if args.mode == 'dvbt':
        if hasattr(sys.stdout, 'buffer'):
            sys.stdout = sys.stdout.buffer
        tb = DVBTReceiver(args)
        tb.start()
        try:
            tb.wait()
        except KeyboardInterrupt:
            tb.stop()
            tb.wait()

    elif args.mode == 'analog':
        if hasattr(sys.stdout, 'buffer'):
            sys.stdout = sys.stdout.buffer
        tb = AnalogTVReceiver(args)
        tb.start()
        try:
            tb.wait()
        except KeyboardInterrupt:
            tb.stop()
            tb.wait()

    elif args.mode == 'spectrum':
        tb = SpectrumAnalyzer(args)
        tb.start()
        try:
            tb.display_loop()
        except KeyboardInterrupt:
            pass
        tb.stop()
        tb.wait()

    elif args.mode == 'camera':
        tb = CameraScanner(args)
        tb.start()
        try:
            tb.scan_loop()
        except KeyboardInterrupt:
            pass
        tb.stop()
        tb.wait()

    elif args.mode == 'camera-rx':
        if hasattr(sys.stdout, 'buffer'):
            sys.stdout = sys.stdout.buffer
        tb = CameraVideoReceiver(args)
        tb.start()
        try:
            tb.wait()
        except KeyboardInterrupt:
            tb.stop()
            tb.wait()


if __name__ == '__main__':
    main()
