gr-apollo/examples/test_signal_gen_demo.py
Ryan Malloy 0ee7ff0ad7 Implement full Apollo USB downlink decoder chain
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
2026-02-20 13:18:42 -07:00

72 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""
Apollo Test Signal Generator Demo — pure Python, no GNU Radio needed.
Generates synthetic USB baseband signals and analyzes them spectrally.
Useful for verifying the signal generator and understanding the signal structure.
Usage:
uv run python examples/test_signal_gen_demo.py
"""
import numpy as np
from apollo.constants import (
PCM_HIGH_BIT_RATE,
PCM_HIGH_WORDS_PER_FRAME,
PCM_SUBCARRIER_HZ,
PCM_WORD_LENGTH,
SAMPLE_RATE_BASEBAND,
VOICE_SUBCARRIER_HZ,
)
from apollo.usb_signal_gen import generate_usb_baseband
def main():
print("Apollo USB Signal Generator Demo")
print("=" * 50)
# Generate a clean signal (no noise)
print("\n1. Clean PCM-only signal (3 frames):")
signal, bits = generate_usb_baseband(frames=3, snr_db=None)
frame_bits = PCM_HIGH_WORDS_PER_FRAME * PCM_WORD_LENGTH
expected_samples = 3 * int(frame_bits * SAMPLE_RATE_BASEBAND / PCM_HIGH_BIT_RATE)
print(f" Samples: {len(signal)} (expected {expected_samples})")
print(f" Duration: {len(signal)/SAMPLE_RATE_BASEBAND*1000:.1f} ms")
print(f" Envelope std: {np.std(np.abs(signal)):.4f} (PM = near-constant)")
# Analyze spectrum
print("\n2. Spectral analysis:")
fft = np.fft.fft(signal[:50000])
freqs = np.fft.fftfreq(50000, 1.0 / SAMPLE_RATE_BASEBAND)
power = np.abs(fft) ** 2
# Check PCM subcarrier band
pcm_mask = (np.abs(freqs) > 950_000) & (np.abs(freqs) < 1_100_000)
pcm_power = np.mean(power[pcm_mask])
total_power = np.mean(power)
print(f" PCM band (950-1100 kHz): {10*np.log10(pcm_power/total_power):.1f} dB re total")
# Generate with voice
print("\n3. Signal with voice subcarrier:")
signal_v, _ = generate_usb_baseband(frames=3, voice_enabled=True, snr_db=None)
fft_v = np.fft.fft(signal_v[:50000])
voice_mask = (np.abs(freqs) > 1_200_000) & (np.abs(freqs) < 1_300_000)
voice_power = np.mean(np.abs(fft_v[voice_mask]) ** 2)
print(f" Voice band (1.2-1.3 MHz): {10*np.log10(voice_power/total_power):.1f} dB re total")
# Generate with noise
print("\n4. Signal with 20 dB SNR noise:")
signal_n, _ = generate_usb_baseband(frames=3, snr_db=20.0)
print(f" Envelope std: {np.std(np.abs(signal_n)):.4f} (noisy = higher variance)")
print("\nKey frequencies:")
print(f" Sample rate: {SAMPLE_RATE_BASEBAND/1e6:.2f} MHz")
print(f" PCM subcarrier: {PCM_SUBCARRIER_HZ/1e6:.3f} MHz")
print(f" Voice subcarrier: {VOICE_SUBCARRIER_HZ/1e6:.3f} MHz")
print(f" PCM bit rate: {PCM_HIGH_BIT_RATE/1000:.1f} kbps")
if __name__ == "__main__":
main()