""" Apollo PM Demodulator — extracts phase modulation from complex baseband. The spacecraft transmitter phase-modulates a 76.25 MHz carrier at 0.133 rad peak deviation (7.6 degrees). After frequency multiplication (×30) to 2287.5 MHz and downconversion to complex baseband at the receiver, this block recovers the composite modulating signal containing all subcarriers. At 0.133 rad, the small-angle approximation holds (sin(0.133) ≈ 0.1327, <0.3% error), so the demodulated output is essentially linear with the modulating signal. Signal chain: complex baseband → carrier PLL → phase extraction → float output Reference: IMPLEMENTATION_SPEC.md section 2.3 """ from gnuradio import analog, blocks, gr class pm_demod(gr.hier_block2): """Phase modulation demodulator with carrier recovery. Inputs: complex baseband (e.g., from SDR or usb_signal_gen) Outputs: float — demodulated composite signal containing all subcarriers """ def __init__(self, carrier_pll_bw: float = 0.02, sample_rate: float = 5_120_000): gr.hier_block2.__init__( self, "apollo_pm_demod", gr.io_signature(1, 1, gr.sizeof_gr_complex), gr.io_signature(1, 1, gr.sizeof_float), ) # Carrier tracking PLL — locks to the residual carrier in the PM signal. # The PLL bandwidth needs to be narrow enough to track carrier drift # but wide enough for acquisition. 0.02 rad/sample is a good default # for the 5.12 MHz sample rate. # # PLL freq range: ±carrier_pll_bw * sample_rate / (2*pi) Hz max_freq = carrier_pll_bw * 2.0 min_freq = -max_freq self.pll = analog.pll_carriertracking_cc(carrier_pll_bw, max_freq, min_freq) # Extract instantaneous phase: atan2(Im, Re) self.phase = blocks.complex_to_arg(1) # Connect: input → PLL → phase extraction → output self.connect(self, self.pll, self.phase, self) def get_carrier_pll_bw(self) -> float: return self.pll.get_loop_bandwidth() def set_carrier_pll_bw(self, bw: float): self.pll.set_loop_bandwidth(bw)