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
78 lines
1.8 KiB
Python
78 lines
1.8 KiB
Python
"""Shared test fixtures for gr-apollo."""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from apollo.constants import (
|
|
PCM_HIGH_BIT_RATE,
|
|
PCM_HIGH_WORDS_PER_FRAME,
|
|
PCM_LOW_BIT_RATE,
|
|
PCM_LOW_WORDS_PER_FRAME,
|
|
SAMPLE_RATE_BASEBAND,
|
|
)
|
|
from apollo.usb_signal_gen import generate_pcm_frame, generate_usb_baseband
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_rate():
|
|
return SAMPLE_RATE_BASEBAND
|
|
|
|
|
|
@pytest.fixture
|
|
def high_rate_params():
|
|
return {
|
|
"bit_rate": PCM_HIGH_BIT_RATE,
|
|
"words_per_frame": PCM_HIGH_WORDS_PER_FRAME,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def low_rate_params():
|
|
return {
|
|
"bit_rate": PCM_LOW_BIT_RATE,
|
|
"words_per_frame": PCM_LOW_WORDS_PER_FRAME,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def known_payload():
|
|
"""A known 124-byte payload (words 5-128) for frame verification."""
|
|
np.random.seed(42)
|
|
return bytes(np.random.randint(0, 256, size=124, dtype=np.uint8))
|
|
|
|
|
|
@pytest.fixture
|
|
def single_frame_bits(known_payload):
|
|
"""A single high-rate frame with known payload, as bit list."""
|
|
return generate_pcm_frame(frame_id=1, odd=True, data=known_payload)
|
|
|
|
|
|
@pytest.fixture
|
|
def clean_baseband(known_payload):
|
|
"""Clean (no noise) single-frame baseband signal with known payload."""
|
|
signal, frame_bits = generate_usb_baseband(
|
|
frames=1,
|
|
frame_data=[known_payload],
|
|
snr_db=None,
|
|
)
|
|
return signal, frame_bits[0]
|
|
|
|
|
|
@pytest.fixture
|
|
def noisy_baseband(known_payload):
|
|
"""Noisy (20 dB SNR) single-frame baseband signal."""
|
|
signal, frame_bits = generate_usb_baseband(
|
|
frames=1,
|
|
frame_data=[known_payload],
|
|
snr_db=20.0,
|
|
)
|
|
return signal, frame_bits[0]
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_frame_baseband():
|
|
"""5-frame baseband signal for frame sync testing."""
|
|
np.random.seed(123)
|
|
signal, frame_bits = generate_usb_baseband(frames=5, snr_db=30.0)
|
|
return signal, frame_bits
|