#!/usr/bin/env python3
"""
modes_iq_samples.py - Stream Mode S frames with I/Q samples from a GRX receiver.

This example subscribes to Mode S downlink frames and additionally
requests raw I/Q sample data for matching transmissions. I/Q samples
allow further signal processing such as direction finding or signal
quality analysis.

Usage:
    python modes_iq_samples.py [HOST]

Arguments:
    HOST    IP address or hostname of the GRX (default: YOUR_GRX_IP)

Prerequisites:
    pip install grpcio grpcio-tools pyModeS
    Run gen_pb.sh to generate the Python bindings.

Note:
    I/Q samples are bandwidth-intensive. Use specific downlink format
    and address filters to avoid overloading the network link. Monitor
    the frames_dropped counter to detect data loss.
"""

import binascii
import struct
import sys

import grpc
import pyModeS as pms

# Import the generated gRPC stubs for the Receiver API
import Receiverd_pb2 as receiverd_pb2
import Receiverd_pb2_grpc

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

host = sys.argv[1] if len(sys.argv) >= 2 else "YOUR_GRX_IP"
RECEIVERD_PORT = 5303

# Subscribe to DF17 (ADS-B) and DF11 (All-Call Reply) only
DOWNLINK_FORMATS = [11, 17]

# Number of frames with I/Q samples to collect before stopping
MAX_SAMPLES = 10

# ---------------------------------------------------------------------------
# Connect and subscribe with I/Q sample rules
# ---------------------------------------------------------------------------

print(f"Connecting to {host}:{RECEIVERD_PORT}...")
channel = grpc.insecure_channel(f"{host}:{RECEIVERD_PORT}")
receiverd = Receiverd_pb2_grpc.ReceiverdStub(channel)

# Build the request with I/Q sample subscription rules.
# A SampleEnableRule defines which frames should include raw I/Q data.
request = receiverd_pb2.GetModeSDownlinkFramesRequest(
    downlink_formats=DOWNLINK_FORMATS,
    sample_enable_rules=[
        # Rule 1: Request I/Q samples for ALL DF17 frames from any aircraft.
        # Setting the address qualifier to MATCH_ANY_ADDRESS disables
        # address filtering (the address field is ignored).
        receiverd_pb2.GetModeSDownlinkFramesRequest.SampleEnableRule(
            address=receiverd_pb2.QualifiedAddressModeS(
                aq=receiverd_pb2.QualifiedAddressModeS.AddressQualifier.MATCH_ANY_ADDRESS
            ),
            downlink_formats=[17],
        ),
    ],
)

stream = receiverd.GetModeSDownlinkFrames(request)

print(f"Subscribed to DF{DOWNLINK_FORMATS} with I/Q samples for DF17.")
print(f"Collecting {MAX_SAMPLES} frames with samples...\n")

# ---------------------------------------------------------------------------
# Process incoming frames
# ---------------------------------------------------------------------------

samples_collected = 0
last_dropped = 0

try:
    for reply in stream:
        # Monitor for dropped frames
        if reply.frames_dropped > last_dropped:
            dropped = reply.frames_dropped - last_dropped
            print(f"  ** WARNING: {dropped} frame(s) dropped **")
            last_dropped = reply.frames_dropped

        frame = reply.frame
        payload_hex = binascii.hexlify(frame.payload).decode("ascii")

        # I/Q samples are in frame.samples.samples (bytes).
        # The format is interleaved signed 16-bit integers: I0, Q0, I1, Q1, ...
        # Each I/Q pair is 4 bytes, so the number of complex samples is len/4.
        raw_samples = frame.samples.samples
        num_iq_samples = len(raw_samples) // 4

        # Only print detailed info for frames that include samples
        if num_iq_samples > 0:
            samples_collected += 1

            # Parse the first few I/Q pairs for display
            num_preview = min(5, num_iq_samples)
            iq_pairs = struct.unpack(f"<{num_preview * 2}h", raw_samples[:num_preview * 4])

            print(f"Frame {samples_collected}/{MAX_SAMPLES}:")
            print(f"  Payload:          {payload_hex}")
            print(f"  DF/ICAO:          DF{pms.df(payload_hex)} / {pms.icao(payload_hex)}")
            print(f"  Signal level:     {frame.level_signal:.1f} dBm")
            print(f"  Noise level:      {frame.level_noise:.1f} dBm")
            print(f"  Carrier offset:   {frame.carrier_frequency_offset:+.0f} Hz")
            print(f"  I/Q samples:      {num_iq_samples}")
            print(f"  Sample rate:      {frame.samples.sample_rate} Hz")
            print(f"  Sample format:    {frame.samples.sample_format}")
            print(f"  First {num_preview} I/Q pairs: ", end="")
            for i in range(num_preview):
                print(f"({iq_pairs[2*i]:+6d}, {iq_pairs[2*i+1]:+6d}) ", end="")
            print("\n")

            if samples_collected >= MAX_SAMPLES:
                break

except KeyboardInterrupt:
    print("\nStopped.")

print(f"Collected {samples_collected} frames with I/Q data.")

# ---------------------------------------------------------------------------
# Clean up
# ---------------------------------------------------------------------------

stream.cancel()
channel.close()
