#!/usr/bin/env python3
"""
mode_ac.py - Stream Mode 1/2/3/A/C downlink replies from a GRX receiver.

This example subscribes to Mode 1/2/3/A/C (Secondary Surveillance Radar)
replies. The receiver groups ("bins") replies with the same code that
arrive within a short time window (~10 ms), providing each individual
reception with its own timestamp and signal level.

Usage:
    python mode_ac.py [HOST]

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

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

Note:
    Mode 1/2/3/A/C data is only available on receivers with a 1030 MHz or
    1090 MHz radio front-end that supports Mode 1/2/3/A/C detection.
"""

import math
import sys
import time
import datetime

import grpc

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

# ---------------------------------------------------------------------------
# Utility: convert GPS nanosecond-of-week timestamp to datetime
# ---------------------------------------------------------------------------

# GPS leap seconds offset from UTC (18 seconds as of Dec 2016)
GPS_LEAP_SECONDS = 18

def gps_ns_to_datetime(timestamp_ns, gps_offset=GPS_LEAP_SECONDS):
    """
    Convert a GPS nanosecond-of-week timestamp to a Python datetime.

    The GRX provides timestamps as nanoseconds since the start of the
    current GPS week. This function determines the start-of-week from
    the current system clock and adds the offset.
    """
    # Unix epoch (1970-01-01) was a Thursday; GPS weeks start on Sunday.
    # Add 345600 s (4 days) to align to GPS week boundaries.
    ref = time.time() + 345600
    start_of_week = math.floor(ref / 604800) * 604800 - 345600 - gps_offset
    return datetime.datetime.fromtimestamp(start_of_week + timestamp_ns / 1e9)

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

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

# ---------------------------------------------------------------------------
# Connect and subscribe
# ---------------------------------------------------------------------------

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

# Subscribe to all Mode 1/2/3/A/C replies (no additional filtering available)
request = receiverd_pb2.GetModeACDownlinkFramesRequest()
stream = receiverd.GetModeACDownlinkFrames(request)

print("Subscribed to Mode 1/2/3/A/C downlink frames. Press Ctrl+C to stop.\n")

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

last_dropped = 0
try:
    for reply in stream:
        # Check 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
        utc = gps_ns_to_datetime(frame.timestamp)

        # The receiver bins multiple receptions of the same A/C code.
        # Each reception has its own timestamp and signal level.
        num_receptions = len(frame.receptions)
        print(f"A/C code={frame.code:04o}  Time={utc}  Receptions={num_receptions}:")

        for i, reception in enumerate(frame.receptions):
            # Time difference from the first reception in the bin
            dt_ms = (reception.timestamp - frame.timestamp) / 1e6
            print(f"  #{i+1}: {reception.signal_level:.1f} dBm  "
                  f"(+{dt_ms:.1f} ms from first)")

except KeyboardInterrupt:
    print("\nStopped.")

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

stream.cancel()
channel.close()
