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
72 lines
2.6 KiB
Python
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()
|