Complete signal processing pipeline from complex baseband to decoded PCM telemetry, verified against the 1965 NAA Study Guide (A-624): Core demod (Phase 1): - PM demodulator with carrier PLL recovery - 1.024 MHz subcarrier extractor (bandpass + downconvert) - BPSK demodulator with Costas loop + symbol sync - Convenience hier_block2 combining subcarrier + BPSK PCM frame processing (Phase 2): - 32-bit frame sync with Hamming distance correlator - SEARCH/VERIFY/LOCKED state machine, complement-on-odd handling - Frame demultiplexer (128-word, A/D voltage scaling) - AGC downlink decoder (15-bit word reassembly from DNTM1/DNTM2) Voice and analog (Phase 3): - 1.25 MHz FM voice subcarrier demod to 8 kHz audio - SCO demodulator for 9 analog sensor channels (14.5-165 kHz) Virtual AGC integration (Phase 4): - TCP bridge client with auto-reconnect and channel filtering - DSKY uplink encoder (VERB/NOUN/DATA command sequences) Top-level receiver (Phase 5): - usb_downlink_receiver hier_block2: one block, complex in, PDUs out - 14 GRC block YAML definitions for GNU Radio Companion - Example scripts for signal analysis and full-chain demo Infrastructure: - constants.py with all timing/frequency/frame parameters - protocol.py for sync word + AGC packet encode/decode - Synthetic USB signal generator for testing - 222 tests passing, ruff lint clean
65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""
|
|
Apollo BPSK Subcarrier Demodulator — convenience wrapper.
|
|
|
|
Hierarchical block combining subcarrier_extract + bpsk_demod into a single
|
|
block for the common case of extracting and demodulating the 1.024 MHz PCM
|
|
subcarrier from the PM demodulator output.
|
|
|
|
This resolves the naming used in __init__.py and provides a simple
|
|
"float in, bytes out" interface.
|
|
|
|
Reference: IMPLEMENTATION_SPEC.md section 4.2
|
|
"""
|
|
|
|
from gnuradio import gr
|
|
|
|
from apollo.bpsk_demod import bpsk_demod
|
|
from apollo.constants import PCM_BPF_BW_HZ, PCM_HIGH_BIT_RATE, PCM_SUBCARRIER_HZ
|
|
from apollo.subcarrier_extract import subcarrier_extract
|
|
|
|
|
|
class bpsk_subcarrier_demod(gr.hier_block2):
|
|
"""Combined subcarrier extraction + BPSK demodulation.
|
|
|
|
Inputs:
|
|
float — PM demodulator output (composite subcarrier signal)
|
|
|
|
Outputs:
|
|
byte — recovered NRZ bits (0 or 1)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
subcarrier_freq: float = PCM_SUBCARRIER_HZ,
|
|
bandwidth: float = PCM_BPF_BW_HZ,
|
|
bit_rate: float = PCM_HIGH_BIT_RATE,
|
|
sample_rate: float = 5_120_000,
|
|
decimation: int = 1,
|
|
loop_bw: float = 0.045,
|
|
):
|
|
gr.hier_block2.__init__(
|
|
self,
|
|
"apollo_bpsk_subcarrier_demod",
|
|
gr.io_signature(1, 1, gr.sizeof_float),
|
|
gr.io_signature(1, 1, gr.sizeof_char),
|
|
)
|
|
|
|
# Subcarrier extraction: bandpass + translate to baseband
|
|
self.extract = subcarrier_extract(
|
|
center_freq=subcarrier_freq,
|
|
bandwidth=bandwidth,
|
|
sample_rate=sample_rate,
|
|
decimation=decimation,
|
|
)
|
|
|
|
# BPSK demodulation: carrier recovery + symbol sync + slicer
|
|
output_rate = sample_rate / decimation
|
|
self.demod = bpsk_demod(
|
|
symbol_rate=bit_rate,
|
|
sample_rate=output_rate,
|
|
loop_bw=loop_bw,
|
|
)
|
|
|
|
# Connect: float input → extract → demod → byte output
|
|
self.connect(self, self.extract, self.demod, self)
|