Integrate uplink chain and PRN ranging into shared files and docs
Consolidate locally-defined constants into constants.py (single source of truth for all uplink modulation and ranging parameters). Update __init__.py with new imports across all three tiers. Add signal architecture, block reference, and demo guide documentation for the uplink and ranging subsystems.
This commit is contained in:
parent
3dc8afdc08
commit
cfc9ca03eb
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Signal Architecture"
|
title: "Signal Architecture"
|
||||||
description: "How the Apollo Unified S-Band downlink signal is structured, and how gr-apollo decomposes it into telemetry"
|
description: "How the Apollo Unified S-Band signal is structured -- downlink, uplink, and ranging -- and how gr-apollo decomposes it"
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
@ -282,3 +282,158 @@ graph LR
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `fm_signal_source` and `fm_downlink_receiver` convenience blocks wire the full chain together, just as `usb_signal_source` and `usb_downlink_receiver` do for PM mode. The FM receiver uses streaming float outputs (one per SCO channel) rather than PDU messages, since SCO telemetry is continuous analog data.
|
The `fm_signal_source` and `fm_downlink_receiver` convenience blocks wire the full chain together, just as `usb_signal_source` and `usb_downlink_receiver` do for PM mode. The FM receiver uses streaming float outputs (one per SCO channel) rather than PDU messages, since SCO telemetry is continuous analog data.
|
||||||
|
|
||||||
|
## The uplink signal
|
||||||
|
|
||||||
|
The downlink carries telemetry from spacecraft to ground. The uplink does the reverse -- delivering commands from Mission Control to the spacecraft on 2106.40625 MHz.
|
||||||
|
|
||||||
|
The modulation scheme is the same family as the downlink (PM carrier with FM subcarriers), but the parameters are dramatically different. Where the downlink tiptoes along at 0.133 rad peak deviation, the uplink runs at 1.0 rad. This asymmetry is not a design inconsistency -- it reflects a fundamental physical reality. The ground station has megawatt-class transmitters and 26-meter dish antennas. The spacecraft has a 20-watt traveling-wave tube and a dish measured in inches. The ground can afford the power overhead of higher modulation index; the spacecraft cannot waste a single fraction of a watt.
|
||||||
|
|
||||||
|
The uplink carries two information streams:
|
||||||
|
|
||||||
|
- **Command data** -- DSKY commands encoded as 15-bit AGC words, transmitted at 2 kbps NRZ on a 70 kHz FM subcarrier with +/-4 kHz deviation
|
||||||
|
- **Voice** (optional) -- Crew-directed audio on a 30 kHz FM subcarrier with +/-7.5 kHz deviation
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "TX — Ground Station"
|
||||||
|
A["DSKY Commands<br/>15-bit AGC words"] --> B["Word Serializer<br/>15 bits → NRZ<br/>2 kbps"]
|
||||||
|
B --> C["NRZ Encoder<br/>0/1 → +1/-1"]
|
||||||
|
C --> D["FM Mod<br/>70 kHz<br/>±4 kHz dev"]
|
||||||
|
D --> E["Σ"]
|
||||||
|
V1["Voice Audio<br/>300-3000 Hz"] -->|"FM<br/>30 kHz<br/>±7.5 kHz dev"| E
|
||||||
|
E --> F["PM Mod<br/>1.0 rad peak"]
|
||||||
|
F --> G["2106.4 MHz<br/>RF Carrier"]
|
||||||
|
end
|
||||||
|
|
||||||
|
G -->|"240/221<br/>turnaround"| H
|
||||||
|
|
||||||
|
subgraph "RX — Spacecraft"
|
||||||
|
H["RF Input<br/>2106.4 MHz"] --> I["PM Demod<br/>Carrier PLL"]
|
||||||
|
I --> J["Extract<br/>70 kHz"]
|
||||||
|
J --> K["FM Demod"]
|
||||||
|
K --> L["Slicer<br/>2 kbps"]
|
||||||
|
L --> M["Word<br/>Deserializer"]
|
||||||
|
M --> N["AGC<br/>Commands"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#2d5016,stroke:#4a8c2a
|
||||||
|
style V1 fill:#2d5016,stroke:#4a8c2a
|
||||||
|
style G fill:#1a3a5c,stroke:#3a7abd
|
||||||
|
style H fill:#1a3a5c,stroke:#3a7abd
|
||||||
|
style F fill:#3a1a5c,stroke:#7a3abd
|
||||||
|
style I fill:#3a1a5c,stroke:#7a3abd
|
||||||
|
style N fill:#5c3a1a,stroke:#bd7a3a
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="TX Blocks">
|
||||||
|
The uplink transmit chain assembles commands into the modulated carrier:
|
||||||
|
|
||||||
|
1. **uplink_encoder** -- Accepts DSKY command PDUs and formats them as 15-bit AGC uplink words. The word format matches the Apollo AGC's INLINK channel (channel 45) protocol.
|
||||||
|
2. **uplink_word_serializer** -- Serializes 15-bit words into a continuous NRZ bit stream at 2 kbps. Each bit is held for the full bit period.
|
||||||
|
3. **nrz_encoder** -- Converts the 0/1 byte stream to +1.0/-1.0 floats, identical to the downlink NRZ encoder.
|
||||||
|
4. **FM modulator** -- Frequency-modulates the NRZ data onto a 70 kHz subcarrier with +/-4 kHz deviation.
|
||||||
|
5. **PM modulator** -- Phase-modulates the composite subcarrier signal onto the carrier at 1.0 rad peak deviation.
|
||||||
|
|
||||||
|
The `usb_uplink_source` hierarchical block wires this chain together.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="RX Blocks">
|
||||||
|
The uplink receiver disassembles the signal in reverse order:
|
||||||
|
|
||||||
|
1. **PM demodulator** -- Same carrier PLL approach as the downlink, but configured for the wider 1.0 rad deviation. The larger modulation index means the small-angle approximation no longer holds (15.9% error at 1.0 rad), but FM demodulation of the subcarrier is tolerant of this distortion.
|
||||||
|
2. **70 kHz extraction** -- Bandpass filter centered on the command subcarrier.
|
||||||
|
3. **FM demodulator** -- Recovers the NRZ data stream from the 70 kHz subcarrier.
|
||||||
|
4. **Slicer and deserializer** -- Threshold detection at 2 kbps, then reassembly of 15-bit AGC words from the serial bit stream.
|
||||||
|
|
||||||
|
The `usb_uplink_receiver` hierarchical block wires this chain together.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The 1.0 rad uplink deviation pushes well beyond the linear small-angle regime. At 1.0 rad, `sin(theta)` deviates from `theta` by nearly 16%. The designers accepted this because the uplink data rate is only 2 kbps (versus 51.2 kbps on the downlink) and FM demodulation is inherently robust against amplitude and phase nonlinearity. The uplink could afford to trade linearity for link margin.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## PRN ranging
|
||||||
|
|
||||||
|
Voice and telemetry tell the ground station what the spacecraft is doing. Ranging tells them where it is. The Apollo USB system measures Earth-to-spacecraft distance by timing how long a known signal takes to make the round trip.
|
||||||
|
|
||||||
|
The ranging signal is a composite pseudo-random noise (PRN) code, transmitted by the ground station and transponded (retransmitted) by the spacecraft. The ground station correlates the received code against its own reference copy, and the time offset between them is the two-way light-time delay.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["PRN Generator<br/>~994 kchip/s"] --> B["NRZ Encode<br/>0/1 → +1/-1"]
|
||||||
|
B --> C["PM Mod onto<br/>Uplink Carrier"]
|
||||||
|
C --> D["Spacecraft<br/>Transponder"]
|
||||||
|
D --> E["PM Mod onto<br/>Downlink Carrier"]
|
||||||
|
E --> F["Ground RX<br/>PRN Correlator"]
|
||||||
|
F --> G["Range<br/>Measurement"]
|
||||||
|
|
||||||
|
A --> H["Reference Copy<br/>(delayed)"]
|
||||||
|
H --> F
|
||||||
|
|
||||||
|
style A fill:#5c3a1a,stroke:#bd7a3a
|
||||||
|
style D fill:#1a3a5c,stroke:#3a7abd
|
||||||
|
style F fill:#3a1a5c,stroke:#7a3abd
|
||||||
|
style G fill:#2d5016,stroke:#4a8c2a
|
||||||
|
```
|
||||||
|
|
||||||
|
### The composite code
|
||||||
|
|
||||||
|
The PRN code is not a single sequence -- it is five component codes combined with Boolean logic:
|
||||||
|
|
||||||
|
| Code | Length (chips) | Type |
|
||||||
|
|------|----------------|------|
|
||||||
|
| CL | 2 | Clock (alternating 0,1) |
|
||||||
|
| X | 11 | Fixed pattern |
|
||||||
|
| A | 31 | Maximal-length LFSR |
|
||||||
|
| B | 63 | Maximal-length LFSR |
|
||||||
|
| C | 127 | Maximal-length LFSR |
|
||||||
|
|
||||||
|
The combination logic produces one output chip per clock:
|
||||||
|
|
||||||
|
```
|
||||||
|
output = (NOT(X) AND majority(A, B, C)) XOR CL
|
||||||
|
```
|
||||||
|
|
||||||
|
where `majority(A, B, C)` outputs 1 when two or more inputs are 1. The combined code length is the product of the component lengths: 2 x 11 x 31 x 63 x 127 = **5,456,682 chips**. At the chip rate of approximately 994 kchip/s, one full code period takes about 5.49 seconds.
|
||||||
|
|
||||||
|
The maximal-length LFSR sequences (A, B, C) give the composite code excellent autocorrelation properties -- a sharp peak when aligned, low sidelobes everywhere else. This is what makes unambiguous range measurement possible even at the signal-to-noise ratios encountered at lunar distance.
|
||||||
|
|
||||||
|
### Sequential acquisition
|
||||||
|
|
||||||
|
Correlating against the full 5.4-million-chip code in one pass would be computationally ruinous, even by modern standards. The Apollo system uses a sequential strategy that exploits the composite structure:
|
||||||
|
|
||||||
|
1. **Correlate CL first** (length 2) -- resolves range to within half the CL period
|
||||||
|
2. **Correlate X** (length 11) -- refines within the CL ambiguity window
|
||||||
|
3. **Correlate A** (length 31) -- further refinement
|
||||||
|
4. **Correlate B** (length 63) -- further refinement
|
||||||
|
5. **Correlate C** (length 127) -- final resolution
|
||||||
|
|
||||||
|
Each stage resolves the ambiguity left by the previous one. The total number of trial positions searched is 2 + 11 + 31 + 63 + 127 = 234, rather than 5,456,682. This makes acquisition practical in seconds rather than hours.
|
||||||
|
|
||||||
|
### Range resolution
|
||||||
|
|
||||||
|
The two-way range resolution is determined by the chip rate:
|
||||||
|
|
||||||
|
```
|
||||||
|
resolution = c / (2 x chip_rate) = 299,792,458 / (2 x 994,000) ~ 150.8 meters
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the minimum distinguishable range difference. The actual measurement precision is much better than this, because the correlator interpolates between chips. NASA routinely achieved ranging precision on the order of 15 meters to the Moon -- about one-tenth of a chip.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Ken Shirriff's [detailed analysis of the Apollo ranging system](https://www.righto.com/2022/09/the-ranging-system-that-measured.html) traces the code generation logic down to the gate level. His work on reverse-engineering the actual hardware provides an invaluable cross-reference for validating the gr-apollo implementation.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### gr-apollo ranging blocks
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Transmit">
|
||||||
|
- **ranging_source** -- Generates the composite PRN chip stream by clocking the five component code generators and combining their outputs. Configurable chip rate (default ~994 kchip/s). Output is one byte per chip (0 or 1).
|
||||||
|
- **ranging_mod** -- NRZ-encodes the chip stream (+1/-1) and scales it for injection into the PM modulator's composite baseband signal.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Receive">
|
||||||
|
- **ranging_demod** -- Performs FFT-based cross-correlation between the received ranging signal and a locally generated reference. Sequential acquisition is implemented as a state machine that steps through the CL, X, A, B, C stages. Outputs range measurement PDUs containing the measured delay in chips, the equivalent range in meters, and a confidence metric.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: "Run the Demos"
|
title: "Run the Demos"
|
||||||
description: "How to run gr-apollo's example scripts for loopback testing, voice modulation, full downlink simulation, and AGC integration."
|
description: "How to run gr-apollo's example scripts for loopback testing, voice modulation, full downlink simulation, AGC integration, uplink commands, and PRN ranging."
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Steps, Aside, Tabs, TabItem, Code, LinkCard, CardGrid } from '@astrojs/starlight/components';
|
import { Steps, Aside, Tabs, TabItem, Code, LinkCard, CardGrid } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
gr-apollo ships with four demo scripts that exercise different parts of the TX/RX chain. They progress from a self-contained streaming loopback to a full mission simulation with crew voice, and finally to live integration with the Virtual AGC emulator.
|
gr-apollo ships with eight demo scripts that exercise different parts of the TX/RX chain. They progress from a self-contained streaming loopback to a full mission simulation with crew voice, live integration with the Virtual AGC emulator, uplink command encoding, and PRN ranging.
|
||||||
|
|
||||||
| Demo | Requires | What It Does |
|
| Demo | Requires | What It Does |
|
||||||
|------|----------|-------------|
|
|------|----------|-------------|
|
||||||
@ -13,6 +13,10 @@ gr-apollo ships with four demo scripts that exercise different parts of the TX/R
|
|||||||
| `voice_subcarrier_demo.py` | GNU Radio, scipy | Real audio through 1.25 MHz FM |
|
| `voice_subcarrier_demo.py` | GNU Radio, scipy | Real audio through 1.25 MHz FM |
|
||||||
| `full_downlink_demo.py` | GNU Radio, scipy | PCM telemetry + crew voice on one carrier |
|
| `full_downlink_demo.py` | GNU Radio, scipy | PCM telemetry + crew voice on one carrier |
|
||||||
| `agc_loopback_demo.py` | yaAGC (no GR) | Live AGC telemetry over TCP |
|
| `agc_loopback_demo.py` | yaAGC (no GR) | Live AGC telemetry over TCP |
|
||||||
|
| `fetch_apollo_audio.py` | ffmpeg | Download real Apollo recordings from Archive.org |
|
||||||
|
| `real_signal_demo.py` | GNU Radio, scipy | Process real Apollo audio through full USB chain |
|
||||||
|
| `uplink_loopback_demo.py` | GNU Radio | Encode DSKY commands, modulate, demodulate, verify |
|
||||||
|
| `ranging_demo.py` | None (pure Python) | PRN code generation, delay simulation, correlation |
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@ -367,6 +371,401 @@ The `AGCBridgeClient` auto-reconnects with exponential backoff. You can start th
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Audio Downloads
|
||||||
|
|
||||||
|
**Script:** `examples/fetch_apollo_audio.py`
|
||||||
|
**Requires:** ffmpeg
|
||||||
|
|
||||||
|
This utility downloads Apollo 11 audio highlights from the Internet Archive as a FLAC file, then extracts individual clips using ffmpeg. The clips are saved as 48 kHz mono WAV files in `examples/audio/` for use with the other signal processing demos.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["Archive.org\nApollo11Highlights.flac"]:::data --> B["urllib\n(download)"]:::timing --> C["FLAC file\n(local)"]:::data
|
||||||
|
C --> D["ffmpeg\n(seek + extract)"]:::rf --> E["apollo11_liftoff.wav"]:::data
|
||||||
|
C --> F["ffmpeg\n(seek + extract)"]:::rf --> G["apollo11_eagle_has_landed.wav"]:::data
|
||||||
|
C --> H["ffmpeg\n(seek + extract)"]:::rf --> I["apollo11_one_small_step.wav"]:::data
|
||||||
|
|
||||||
|
classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
|
||||||
|
classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
|
||||||
|
classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
Five clips are defined in the script, covering key mission moments from liftoff through splashdown. The FLAC source is removed after extraction by default to save disk space.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python examples/fetch_apollo_audio.py --list
|
||||||
|
uv run python examples/fetch_apollo_audio.py --all
|
||||||
|
uv run python examples/fetch_apollo_audio.py --clip eagle_has_landed
|
||||||
|
uv run python examples/fetch_apollo_audio.py --clip liftoff --force
|
||||||
|
uv run python examples/fetch_apollo_audio.py --all --keep-flac
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Argument | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `--list` | off | List available clip names and timestamps |
|
||||||
|
| `--clip` | -- | Extract a specific clip by name |
|
||||||
|
| `--all` | off | Extract all five defined clips |
|
||||||
|
| `--keep-flac` | off | Keep the downloaded FLAC file after extraction |
|
||||||
|
| `--force` | off | Re-download and re-extract even if files already exist |
|
||||||
|
| `--output-dir` | `examples/audio/` | Output directory for WAV files |
|
||||||
|
|
||||||
|
### Expected Output (--list)
|
||||||
|
|
||||||
|
```
|
||||||
|
Available clips:
|
||||||
|
|
||||||
|
liftoff 00:00:05 (00:00:30) Apollo 11 liftoff
|
||||||
|
eagle_has_landed 00:06:45 (00:00:30) The Eagle has landed
|
||||||
|
one_small_step 00:15:30 (00:00:25) One small step for man
|
||||||
|
houston_problem 00:20:00 (00:00:15) Houston, we've had a problem
|
||||||
|
splashdown 00:42:00 (00:00:20) Splashdown
|
||||||
|
|
||||||
|
5 clips defined.
|
||||||
|
Extract with: --clip NAME or --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Output (--all)
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
Apollo 11 Audio Fetch
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Step 1: Download source FLAC
|
||||||
|
Downloading: https://archive.org/download/Apollo11AudioHighlights/Apollo11Highlights.flac
|
||||||
|
Saving to: examples/audio/Apollo11Highlights.flac
|
||||||
|
|
||||||
|
[########################################] 100.0% 45.2/45.2 MB
|
||||||
|
Downloaded 45.2 MB
|
||||||
|
|
||||||
|
Step 2: Extract 5 clip(s)
|
||||||
|
|
||||||
|
[liftoff] Extracting: Apollo 11 liftoff
|
||||||
|
start=00:00:05 duration=00:00:30
|
||||||
|
-> examples/audio/apollo11_liftoff.wav (2880 KB)
|
||||||
|
[eagle_has_landed] Extracting: The Eagle has landed
|
||||||
|
start=00:06:45 duration=00:00:30
|
||||||
|
-> examples/audio/apollo11_eagle_has_landed.wav (2880 KB)
|
||||||
|
[one_small_step] Extracting: One small step for man
|
||||||
|
start=00:15:30 duration=00:00:25
|
||||||
|
-> examples/audio/apollo11_one_small_step.wav (2400 KB)
|
||||||
|
[houston_problem] Extracting: Houston, we've had a problem
|
||||||
|
start=00:20:00 duration=00:00:15
|
||||||
|
-> examples/audio/apollo11_houston_problem.wav (1440 KB)
|
||||||
|
[splashdown] Extracting: Splashdown
|
||||||
|
start=00:42:00 duration=00:00:20
|
||||||
|
-> examples/audio/apollo11_splashdown.wav (1920 KB)
|
||||||
|
|
||||||
|
Removed source FLAC (45.2 MB). Use --keep-flac to retain.
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
Extracted: 5 Failed: 0
|
||||||
|
Output: examples/audio/apollo11_*.wav
|
||||||
|
============================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Run this script first if you want to use `real_signal_demo.py` with actual Apollo recordings. The bundled `apollo11_crew.wav` clip works as a fallback, but the Archive.org recordings are the real thing.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Real Signal Processing
|
||||||
|
|
||||||
|
**Script:** `examples/real_signal_demo.py`
|
||||||
|
**Requires:** GNU Radio, scipy
|
||||||
|
|
||||||
|
This demo auto-discovers WAV files in `examples/audio/` (downloaded by `fetch_apollo_audio.py`) and runs them through the full USB downlink chain: transmit (NRZ + BPSK + voice FM onto PM carrier) then receive (PCM frame recovery + voice demodulation). It proves the gr-apollo signal chain works on real-world audio, not just synthetic test tones.
|
||||||
|
|
||||||
|
If no downloaded clips are found, the demo falls back to the bundled `examples/audio/apollo11_crew.wav`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph discover ["Audio Discovery"]
|
||||||
|
direction LR
|
||||||
|
A["examples/audio/\napollo11_*.wav"]:::data --> B["auto-discover\n(skip output files)"]:::timing
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph TX ["TX (spacecraft)"]
|
||||||
|
direction LR
|
||||||
|
C["pcm_frame_source\n→ nrz → bpsk_mod"]:::data --> F["add_ff"]:::rf
|
||||||
|
D["real audio clip\n→ resample → upsample"]:::data --> E["fm_voice_mod\n× 0.764"]:::rf --> F
|
||||||
|
F --> G["pm_mod"]:::rf
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph RX ["RX (ground station)"]
|
||||||
|
direction LR
|
||||||
|
H["pm_demod"]:::rf --> I["bpsk_demod\n→ frame_sync"]:::rf
|
||||||
|
H --> J["voice_demod"]:::rf
|
||||||
|
I --> K["PCM frames"]:::data
|
||||||
|
J --> L["recovered WAV"]:::data
|
||||||
|
end
|
||||||
|
|
||||||
|
B --> D
|
||||||
|
G --> H
|
||||||
|
|
||||||
|
classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
|
||||||
|
classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
|
||||||
|
classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python examples/real_signal_demo.py
|
||||||
|
uv run python examples/real_signal_demo.py --clip eagle_has_landed
|
||||||
|
uv run python examples/real_signal_demo.py --snr 25
|
||||||
|
uv run python examples/real_signal_demo.py --clip liftoff --play
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Argument | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `--clip` | first discovered | Process a specific clip by name |
|
||||||
|
| `--snr` | None | Add AWGN noise at this SNR in dB |
|
||||||
|
| `--play` | off | Play recovered audio with `aplay` after processing |
|
||||||
|
|
||||||
|
### Expected Output
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
Apollo Real Signal Demo
|
||||||
|
Full USB downlink: PCM telemetry + crew voice
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Found 5 clip(s): liftoff, eagle_has_landed, one_small_step, houston_problem, splashdown
|
||||||
|
|
||||||
|
Processing: eagle_has_landed
|
||||||
|
------------------------------------------------------------
|
||||||
|
Loading: examples/audio/apollo11_eagle_has_landed.wav
|
||||||
|
Duration: 30.00s, 153,600,000 baseband samples
|
||||||
|
TX: 153,600,000 samples, ~1502 PCM frames, SNR=clean
|
||||||
|
TX complete: 153,600,000 complex samples (28.5s)
|
||||||
|
RX PCM: 1498 frames recovered (41.2s)
|
||||||
|
RX voice: 240,000 samples, 30.00s (25.1s)
|
||||||
|
Saved: examples/audio/apollo11_eagle_has_landed_recovered.wav
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
Summary
|
||||||
|
============================================================
|
||||||
|
Clip: eagle_has_landed
|
||||||
|
Input duration: 30.00s
|
||||||
|
Recovered audio: 30.00s
|
||||||
|
PCM frames: 1498 recovered (expected ~1502)
|
||||||
|
SNR: clean
|
||||||
|
Processing time: TX=28.5s PCM-RX=41.2s Voice-RX=25.1s
|
||||||
|
Output: examples/audio/apollo11_eagle_has_landed_recovered.wav
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Play recovered: aplay examples/audio/apollo11_eagle_has_landed_recovered.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The signal path is identical to `full_downlink_demo.py` -- the difference is that this script auto-discovers audio clips and works with whatever `fetch_apollo_audio.py` has downloaded. If the `examples/audio/` directory only contains the bundled `apollo11_crew.wav`, the demo uses that instead.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uplink Loopback
|
||||||
|
|
||||||
|
**Script:** `examples/uplink_loopback_demo.py`
|
||||||
|
**Requires:** GNU Radio
|
||||||
|
|
||||||
|
This demo exercises the full uplink signal chain. It encodes a DSKY command (V16N36E by default), serializes the words to a bit stream, modulates through the RF path (NRZ, FM onto a 70 kHz data subcarrier, then PM), demodulates on the other end, and deserializes the recovered bits back to uplink words. The TX and RX word sequences are compared for a word-for-word match.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph TX ["TX (ground station)"]
|
||||||
|
direction LR
|
||||||
|
A["UplinkEncoder\n(V16N36E)"]:::data --> B["UplinkSerializer\n(word→bits)"]:::data --> C["nrz_encoder"]:::timing
|
||||||
|
C --> D["FM mod\n(±4 kHz)"]:::rf --> E["upconvert\n70 kHz"]:::rf --> F["pm_mod\n(1.0 rad)"]:::rf
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph RX ["RX (spacecraft)"]
|
||||||
|
direction LR
|
||||||
|
G["pm_demod"]:::rf --> H["subcarrier_extract\n70 kHz"]:::rf --> I["FM demod"]:::rf
|
||||||
|
I --> J["matched filter\n+ slicer"]:::timing --> K["UplinkDeserializer\n(bits→words)"]:::data
|
||||||
|
end
|
||||||
|
|
||||||
|
F --> G
|
||||||
|
|
||||||
|
classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
|
||||||
|
classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
|
||||||
|
classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
The pure-Python engines handle word-to-bit conversion at the endpoints, while the GNU Radio streaming chain proves the RF modulation path works end-to-end.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python examples/uplink_loopback_demo.py
|
||||||
|
uv run python examples/uplink_loopback_demo.py --snr 20
|
||||||
|
uv run python examples/uplink_loopback_demo.py --snr 10 --verb 37 --noun 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Argument | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `--verb` | 16 | Verb number for the DSKY command |
|
||||||
|
| `--noun` | 36 | Noun number for the DSKY command |
|
||||||
|
| `--snr` | None | SNR in dB (None = clean, no noise) |
|
||||||
|
|
||||||
|
### Expected Output
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
Apollo Uplink Loopback Demo
|
||||||
|
============================================================
|
||||||
|
Command: V16N36E
|
||||||
|
Uplink words: 7
|
||||||
|
SNR: clean (no noise)
|
||||||
|
|
||||||
|
TX word sequence:
|
||||||
|
[0] ch=032 val=10000 (4096) bits=001000000000000
|
||||||
|
[1] ch=032 val=10001 (4097) bits=001000000000001
|
||||||
|
[2] ch=032 val=10110 (4168) bits=001000001110000
|
||||||
|
[3] ch=032 val=00011 ( 3) bits=000000000000011
|
||||||
|
[4] ch=032 val=00110 ( 6) bits=000000000000110
|
||||||
|
[5] ch=032 val=11000 (6144) bits=001100000000000
|
||||||
|
[6] ch=032 val=10010 (4106) bits=001000000001010
|
||||||
|
|
||||||
|
Total bits: 8036 (504 data + 7532 idle)
|
||||||
|
Samples per bit: 2560
|
||||||
|
Total samples: 20,572,160
|
||||||
|
Duration: 4.018 s
|
||||||
|
|
||||||
|
Building flowgraph...
|
||||||
|
Running flowgraph (TX -> RX)...
|
||||||
|
|
||||||
|
Recovered 8036 bits from slicer
|
||||||
|
Recovered 7 words (expected 7)
|
||||||
|
|
||||||
|
RX word sequence:
|
||||||
|
[0] ch=032 val=10000 (4096) bits=001000000000000
|
||||||
|
[1] ch=032 val=10001 (4097) bits=001000000000001
|
||||||
|
[2] ch=032 val=10110 (4168) bits=001000001110000
|
||||||
|
[3] ch=032 val=00011 ( 3) bits=000000000000011
|
||||||
|
[4] ch=032 val=00110 ( 6) bits=000000000000110
|
||||||
|
[5] ch=032 val=11000 (6144) bits=001100000000000
|
||||||
|
[6] ch=032 val=10010 (4106) bits=001000000001010
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
Words transmitted: 7
|
||||||
|
Words recovered: 7
|
||||||
|
Matches: 7/7
|
||||||
|
Word error rate: 0.0%
|
||||||
|
------------------------------------------------------------
|
||||||
|
|
||||||
|
V16N36E round-trip: all 7 words match.
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
The bit rate is 2 kbps and the data subcarrier is 70 kHz -- both per the Apollo USB uplink specification. The 0.5-second idle periods before and after the data give the PLL time to settle. If you lower the SNR below about 8 dB, you may start seeing word errors in the recovered sequence.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRN Ranging
|
||||||
|
|
||||||
|
**Script:** `examples/ranging_demo.py`
|
||||||
|
**Requires:** None (pure Python, no GNU Radio needed)
|
||||||
|
|
||||||
|
This demo exercises the Apollo ranging subsystem. It generates the composite PRN ranging code from its five component codes (CL, X, A, B, C), verifies their algebraic properties, NRZ-encodes the chip stream, applies a known propagation delay to simulate spacecraft distance, optionally adds noise, and cross-correlates to recover the delay. The measured range is compared against the true range.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["RangingCodeGenerator\n(CL×X×A×B×C)"]:::data --> B["NRZ encode\n(bipolar ±1)"]:::timing --> C["np.roll\n(apply delay)"]:::rf
|
||||||
|
C --> D["+ AWGN\n(optional)"]:::rf --> E["RangingCorrelator\n(cross-correlate)"]:::data
|
||||||
|
E --> F["range estimate\n(km)"]:::data
|
||||||
|
|
||||||
|
classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
|
||||||
|
classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
|
||||||
|
classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
The demo works at chip rate (1 sample per chip) for simplicity and speed. The composite PRN code length is 2,037,675 chips, giving an unambiguous range that covers Earth-to-Moon distances.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python examples/ranging_demo.py
|
||||||
|
uv run python examples/ranging_demo.py --range-km 100
|
||||||
|
uv run python examples/ranging_demo.py --range-km 384400 # Moon distance
|
||||||
|
uv run python examples/ranging_demo.py --snr 20
|
||||||
|
uv run python examples/ranging_demo.py --chips 200000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Argument | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `--range-km` | 100.0 | Target range in km |
|
||||||
|
| `--snr` | None | SNR in dB (None = clean, no noise) |
|
||||||
|
| `--chips` | 50,000 | Number of chips for correlation |
|
||||||
|
|
||||||
|
### Expected Output
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
Apollo PRN Ranging Demo
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
1. Component code verification
|
||||||
|
----------------------------------------
|
||||||
|
CL: length= 2 (1s=1, 0s=1), periodic=True, balance=OK [OK]
|
||||||
|
X: length= 11 (1s=6, 0s=5), periodic=True, balance=OK [OK]
|
||||||
|
A: length= 5 (1s=3, 0s=2), periodic=True, balance=OK [OK]
|
||||||
|
B: length= 7 (1s=4, 0s=3), periodic=True, balance=OK [OK]
|
||||||
|
C: length= 5 (1s=3, 0s=2), periodic=True, balance=OK [OK]
|
||||||
|
Code length: 2,037,675 = 2*11*5*7*5*... [OK]
|
||||||
|
Composite sample (50,000 chips): balance=0.500 (ideal ~0.5)
|
||||||
|
|
||||||
|
2. Generating 50,000 PRN chips...
|
||||||
|
Generated in 2.3 ms
|
||||||
|
Ones: 25,012 / 50,000 (50.0%)
|
||||||
|
|
||||||
|
3. Simulating range: 100.0 km
|
||||||
|
Round-trip distance: 200.0 km
|
||||||
|
Round-trip time: 0.6671 ms
|
||||||
|
Delay: 6.67 chips (7 samples)
|
||||||
|
Added AWGN noise at 20 dB SNR (noise power = 0.0100)
|
||||||
|
|
||||||
|
4. Cross-correlating...
|
||||||
|
Correlation time: 4.1 ms
|
||||||
|
Peak-to-average ratio: 224.3
|
||||||
|
|
||||||
|
5. Range measurement results
|
||||||
|
----------------------------------------
|
||||||
|
True range: 100.0 km
|
||||||
|
Measured range: 104.9 km
|
||||||
|
Error: 4950.0 m
|
||||||
|
Quantization step: 14984.4 m (1 chip, two-way)
|
||||||
|
Delay (chips): 7.00
|
||||||
|
Delay (samples): 7
|
||||||
|
Correlation peak: 50000
|
||||||
|
|
||||||
|
Error is within one quantization step -- measurement is correct.
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
At chip rate (1 sample per chip), the range quantization step is about 15 km. This is the coarse acquisition mode. The real Apollo system achieved finer resolution by oversampling the chip stream and interpolating the correlation peak. The `--chips` argument controls how many chips are correlated -- more chips improve the peak-to-average ratio and noise resistance, but the quantization step stays the same unless you oversample.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Try `--range-km 384400` to simulate ranging to the Moon. The composite code is long enough to handle the round-trip without ambiguity. With `--snr 10`, you can see how the correlation peak rises above the noise floor even in degraded conditions.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Which Demo to Start With
|
## Which Demo to Start With
|
||||||
|
|
||||||
If you are new to gr-apollo:
|
If you are new to gr-apollo:
|
||||||
@ -379,12 +778,25 @@ If you are new to gr-apollo:
|
|||||||
|
|
||||||
4. **Connect to yaAGC** when you are ready to interact with a running Apollo Guidance Computer.
|
4. **Connect to yaAGC** when you are ready to interact with a running Apollo Guidance Computer.
|
||||||
|
|
||||||
|
5. **Run the ranging demo** to see PRN code generation and correlation at work. It is pure Python with no GNU Radio dependency, so it runs anywhere.
|
||||||
|
|
||||||
|
6. **Try the uplink loopback** to see DSKY commands travel through the RF chain and come back intact on the other side.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
If you want to work with real Apollo recordings rather than the bundled test clip, run `fetch_apollo_audio.py --all` first. The downloaded clips are automatically picked up by `real_signal_demo.py` and can also be passed directly to the voice and full-downlink demos.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
<CardGrid>
|
<CardGrid>
|
||||||
<LinkCard
|
<LinkCard
|
||||||
title="Build a Transmit Signal"
|
title="Build a Transmit Signal"
|
||||||
description="Detailed walkthrough of the TX block chain"
|
description="Detailed walkthrough of the TX block chain"
|
||||||
href="/guides/transmit-signal/"
|
href="/guides/transmit-signal/"
|
||||||
/>
|
/>
|
||||||
|
<LinkCard
|
||||||
|
title="Signal Architecture"
|
||||||
|
description="Uplink, downlink, and ranging signal paths"
|
||||||
|
href="/reference/signal-architecture/"
|
||||||
|
/>
|
||||||
<LinkCard
|
<LinkCard
|
||||||
title="Block Reference"
|
title="Block Reference"
|
||||||
description="Full API docs for all blocks used in the demos"
|
description="Full API docs for all blocks used in the demos"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
|||||||
Every component in gr-apollo falls into one of two categories: **GNU Radio blocks** that require the `gnuradio` runtime, and **pure-Python engines** that work standalone. The engines power the GR blocks internally, but you can also use them directly for testing, scripting, or integration without GNU Radio.
|
Every component in gr-apollo falls into one of two categories: **GNU Radio blocks** that require the `gnuradio` runtime, and **pure-Python engines** that work standalone. The engines power the GR blocks internally, but you can also use them directly for testing, scripting, or integration without GNU Radio.
|
||||||
|
|
||||||
<Aside type="note">
|
<Aside type="note">
|
||||||
Blocks that require GNU Radio are imported lazily. If `gnuradio` is not installed, the pure-Python engines (`FrameSyncEngine`, `DemuxEngine`, `DownlinkEngine`, `AGCBridgeClient`, `UplinkEncoder`, `generate_usb_baseband`) remain available.
|
Blocks that require GNU Radio are imported lazily. If `gnuradio` is not installed, the pure-Python engines (`FrameSyncEngine`, `DemuxEngine`, `DownlinkEngine`, `AGCBridgeClient`, `UplinkEncoder`, `UplinkSerializerEngine`, `UplinkDeserializerEngine`, `RangingCodeGenerator`, `RangingCorrelator`, `generate_usb_baseband`) remain available.
|
||||||
</Aside>
|
</Aside>
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -1467,3 +1467,610 @@ complex in -> fm_demod -> sco_demod(ch1) -> output[0]
|
|||||||
-> sco_demod(ch2) -> output[1]
|
-> sco_demod(ch2) -> output[1]
|
||||||
-> sco_demod(chN) -> output[N-1]
|
-> sco_demod(chN) -> output[N-1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uplink Chain
|
||||||
|
|
||||||
|
The uplink carries ground-station commands to the spacecraft as 15-bit AGC words at 2 kbps NRZ on a 70 kHz FM subcarrier, phase-modulated onto the 2106.40625 MHz uplink carrier at 1.0 rad peak deviation. Each word triggers an UPRUPT interrupt in the AGC flight software.
|
||||||
|
|
||||||
|
### `UplinkSerializerEngine` / `UplinkDeserializerEngine`
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Serializer Engine">
|
||||||
|
|
||||||
|
**Module:** `apollo.uplink_word_codec`
|
||||||
|
**Type:** Pure-Python class
|
||||||
|
**Purpose:** Serialize (channel, value) pairs into a continuous NRZ bit stream with configurable inter-word gap.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.uplink_word_codec import UplinkSerializerEngine
|
||||||
|
|
||||||
|
engine = UplinkSerializerEngine(inter_word_gap=3)
|
||||||
|
engine.add_words([(37, 0x4400), (37, 0x0400)])
|
||||||
|
bits = engine.next_bits(36)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `inter_word_gap` | `int` | `3` | Number of zero-bit periods between consecutive words (`UPLINK_INTER_WORD_GAP`) |
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `add_words` | `(pairs: list[tuple[int, int]]) -> None` | Queue (channel, value) pairs for serialization. Each value is serialized as 15 bits MSB-first, followed by inter-word gap zeros. The channel is not transmitted -- it is used only for metadata/logging |
|
||||||
|
| `next_bits` | `(n: int) -> list[int]` | Pull exactly `n` bits from the queue. Returns queued data bits when available, zeros otherwise (idle carrier) |
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `pending` | `int` | Number of bits remaining in the queue |
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Deserializer Engine">
|
||||||
|
|
||||||
|
**Module:** `apollo.uplink_word_codec`
|
||||||
|
**Type:** Pure-Python class
|
||||||
|
**Purpose:** Reassemble 15-bit AGC words from a recovered NRZ bit stream using a two-phase state machine (acquisition and locked framing).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.uplink_word_codec import UplinkDeserializerEngine
|
||||||
|
|
||||||
|
engine = UplinkDeserializerEngine(inter_word_gap=3)
|
||||||
|
pairs = engine.process_bits([1, 0, 1, 1, ...])
|
||||||
|
# Returns: [(37, 0x4400), ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `inter_word_gap` | `int` | `3` | Expected number of zero bits between words (`UPLINK_INTER_WORD_GAP`) |
|
||||||
|
| `channel` | `int` | `37` | AGC channel to assign to recovered words (`AGC_CH_INLINK`, 045 octal) |
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `process_bits` | `(bits: list[int]) -> list[tuple[int, int]]` | Process a batch of recovered bits. Returns list of (channel, value) tuples for each completed word |
|
||||||
|
| `reset` | `() -> None` | Clear internal state for a fresh decode pass |
|
||||||
|
|
||||||
|
#### State Machine
|
||||||
|
|
||||||
|
The deserializer uses a two-phase approach:
|
||||||
|
|
||||||
|
1. **Acquisition** -- Scans for the first non-zero bit (all DSKY keycodes have at least one set bit in the upper 5 bits, so the first transmitted word always starts with a 1).
|
||||||
|
2. **Locked** -- Uses fixed framing: collects exactly 15 bits per word, skips exactly `inter_word_gap` bits, then collects the next 15, etc. The lock releases after seeing a null word (all zeros), returning to acquisition.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Fixed framing after the first word boundary is necessary because data words can start with leading zeros that would be indistinguishable from the inter-word gap in a bit-synchronous scheme.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `uplink_word_serializer`
|
||||||
|
|
||||||
|
**Module:** `apollo.uplink_word_codec`
|
||||||
|
**Type:** `gr.sync_block`
|
||||||
|
**Purpose:** GNU Radio source block wrapping `UplinkSerializerEngine`. Accepts word PDUs on message input, outputs continuous NRZ byte stream.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.uplink_word_codec import uplink_word_serializer
|
||||||
|
|
||||||
|
blk = uplink_word_serializer(inter_word_gap=3)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I/O Signature
|
||||||
|
|
||||||
|
| Port | Direction | Type | Description |
|
||||||
|
|------|-----------|------|-------------|
|
||||||
|
| (none) | Input | (none) | Source block -- no streaming input |
|
||||||
|
| `out0` | Output | `byte` | NRZ bit stream (values 0 or 1). Zeros when idle |
|
||||||
|
| `"words"` | Input | Message (PDU) | Word injection: PDU with metadata dict containing `"channel"` and `"value"` keys, or a pair `(channel . value)` |
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `inter_word_gap` | `int` | `3` | Number of zero-bit periods between words (`UPLINK_INTER_WORD_GAP`) |
|
||||||
|
|
||||||
|
#### Input PDU Format
|
||||||
|
|
||||||
|
The `"words"` message port accepts the same format emitted by `uplink_encoder`:
|
||||||
|
|
||||||
|
| Key | PMT Type | Description |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| `"channel"` | `pmt.from_long` | AGC channel number (37 for INLINK) |
|
||||||
|
| `"value"` | `pmt.from_long` | 15-bit word value |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `uplink_word_deserializer`
|
||||||
|
|
||||||
|
**Module:** `apollo.uplink_word_codec`
|
||||||
|
**Type:** `gr.basic_block`
|
||||||
|
**Purpose:** GNU Radio block wrapping `UplinkDeserializerEngine`. Consumes byte stream, emits PDU messages for each recovered 15-bit word.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.uplink_word_codec import uplink_word_deserializer
|
||||||
|
|
||||||
|
blk = uplink_word_deserializer(inter_word_gap=3, channel=37)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I/O Signature
|
||||||
|
|
||||||
|
| Port | Direction | Type | Description |
|
||||||
|
|------|-----------|------|-------------|
|
||||||
|
| `in0` | Input | `byte` (streaming) | NRZ bit stream (values 0 or 1) from binary slicer |
|
||||||
|
| `"commands"` | Output | Message (PDU) | Recovered word PDUs |
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `inter_word_gap` | `int` | `3` | Expected number of zero bits between words (`UPLINK_INTER_WORD_GAP`) |
|
||||||
|
| `channel` | `int` | `37` | AGC channel to assign to recovered words (`AGC_CH_INLINK`) |
|
||||||
|
|
||||||
|
#### Output PDU Format
|
||||||
|
|
||||||
|
Each output PDU is `pmt.cons(metadata, pmt.PMT_NIL)`:
|
||||||
|
|
||||||
|
**Metadata dict:**
|
||||||
|
| Key | PMT Type | Description |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| `"channel"` | `pmt.from_long` | AGC channel number (37) |
|
||||||
|
| `"value"` | `pmt.from_long` | Recovered 15-bit word value |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `usb_uplink_source`
|
||||||
|
|
||||||
|
**Module:** `apollo.usb_uplink_source`
|
||||||
|
**Type:** `gr.hier_block2`
|
||||||
|
**Purpose:** Complete Apollo USB uplink transmit chain -- serializes 15-bit AGC words into NRZ bits, FM-modulates onto 70 kHz subcarrier, and applies PM modulation to produce complex baseband.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.usb_uplink_source import usb_uplink_source
|
||||||
|
|
||||||
|
blk = usb_uplink_source(sample_rate=5_120_000, snr_db=20.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I/O Signature
|
||||||
|
|
||||||
|
| Port | Direction | Type | Description |
|
||||||
|
|------|-----------|------|-------------|
|
||||||
|
| (none) | Input | (none) | Source block -- no streaming input |
|
||||||
|
| `out0` | Output | `complex` | PM-modulated complex baseband at `sample_rate` |
|
||||||
|
| `"words"` | Input | Message | Forwarded to `uplink_word_serializer` for command injection. Accepts the same PDU format emitted by `uplink_encoder` |
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `sample_rate` | `float` | `5120000` | Baseband sample rate in Hz (`SAMPLE_RATE_BASEBAND`) |
|
||||||
|
| `bit_rate` | `int` | `2000` | Uplink data bit rate in bps (`UPLINK_DATA_BIT_RATE`) |
|
||||||
|
| `pm_deviation` | `float` | `1.0` | Peak PM deviation in radians (`UPLINK_PM_DEVIATION_RAD`) |
|
||||||
|
| `snr_db` | `float \| None` | `None` | Add AWGN at this SNR. `None` means no noise |
|
||||||
|
|
||||||
|
#### Internal Signal Chain
|
||||||
|
|
||||||
|
```
|
||||||
|
word_ser -> nrz_encoder(2 kbps) -> fm_mod(sensitivity)
|
||||||
|
-> (mixer, 0); sig_source_c(70 kHz) -> (mixer, 1)
|
||||||
|
-> mixer -> complex_to_real -> pm_mod(1.0 rad) -> [+AWGN] -> output
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sub-Block Access
|
||||||
|
|
||||||
|
| Attribute | Type | Block |
|
||||||
|
|-----------|------|-------|
|
||||||
|
| `self.word_ser` | `uplink_word_serializer` | Word serializer |
|
||||||
|
| `self.nrz` | `nrz_encoder` | NRZ line encoder |
|
||||||
|
| `self.fm_mod` | `frequency_modulator_fc` | FM modulator |
|
||||||
|
| `self.lo` | `sig_source_c` | 70 kHz LO |
|
||||||
|
| `self.mixer` | `multiply_cc` | Subcarrier upconverter |
|
||||||
|
| `self.to_real` | `complex_to_real` | Complex-to-real conversion |
|
||||||
|
| `self.pm` | `pm_mod` | Phase modulator |
|
||||||
|
| `self.noise` | `noise_source_c` | AWGN source (when `snr_db` is set) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `usb_uplink_receiver`
|
||||||
|
|
||||||
|
**Module:** `apollo.usb_uplink_receiver`
|
||||||
|
**Type:** `gr.hier_block2`
|
||||||
|
**Purpose:** Complete Apollo USB uplink receiver chain -- complex baseband input to recovered command PDUs. The spacecraft-side counterpart to `usb_uplink_source`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.usb_uplink_receiver import usb_uplink_receiver
|
||||||
|
|
||||||
|
blk = usb_uplink_receiver(sample_rate=5_120_000)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I/O Signature
|
||||||
|
|
||||||
|
| Port | Direction | Type | Description |
|
||||||
|
|------|-----------|------|-------------|
|
||||||
|
| `in0` | Input | `complex` (streaming) | Baseband IQ samples at `sample_rate` |
|
||||||
|
| `"commands"` | Output | Message (PDU) | Decoded (channel, value) PDUs for AGC bridge |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
This block has one streaming input and zero streaming outputs. All output is via the `"commands"` message port.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `sample_rate` | `float` | `5120000` | Input sample rate in Hz (`SAMPLE_RATE_BASEBAND`) |
|
||||||
|
| `bit_rate` | `int` | `2000` | Uplink data bit rate in bps (`UPLINK_DATA_BIT_RATE`) |
|
||||||
|
| `carrier_pll_bw` | `float` | `0.02` | PM demodulator PLL bandwidth (rad/sample) |
|
||||||
|
| `subcarrier_bw` | `float` | `20000` | 70 kHz subcarrier bandpass filter width (Hz) |
|
||||||
|
|
||||||
|
#### Internal Signal Chain
|
||||||
|
|
||||||
|
```
|
||||||
|
complex in -> pm_demod -> subcarrier_extract(70 kHz, BW=20 kHz)
|
||||||
|
-> quadrature_demod_cf(FM) -> matched_filter(1/samp_per_bit)
|
||||||
|
-> keep_one_in_n(samp_per_bit) -> binary_slicer_fb
|
||||||
|
-> uplink_word_deserializer -> "commands" message output
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sub-Block Access
|
||||||
|
|
||||||
|
| Attribute | Type | Block |
|
||||||
|
|-----------|------|-------|
|
||||||
|
| `self.pm` | `pm_demod` | PM demodulator |
|
||||||
|
| `self.sc_extract` | `subcarrier_extract` | 70 kHz subcarrier extractor |
|
||||||
|
| `self.fm_demod` | `quadrature_demod_cf` | FM discriminator |
|
||||||
|
| `self.matched_filter` | `fir_filter_fff` | Integrate-and-dump matched filter |
|
||||||
|
| `self.decimator` | `keep_one_in_n` | Bit-rate decimator |
|
||||||
|
| `self.slicer` | `binary_slicer_fb` | Hard-decision binary slicer |
|
||||||
|
| `self.deser` | `uplink_word_deserializer` | Word reassembler |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRN Ranging
|
||||||
|
|
||||||
|
The Apollo ranging system measures spacecraft distance by transmitting a composite pseudo-random noise (PRN) code and correlating the echo. The code combines five component sequences (CL, X, A, B, C) using majority-vote logic and XOR operations. Combined code length: 5,456,682 chips (~5.49 seconds at 993,963 chips/sec).
|
||||||
|
|
||||||
|
### `RangingCodeGenerator`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging`
|
||||||
|
**Type:** Pure-Python class
|
||||||
|
**Purpose:** Generate Apollo PRN ranging code sequences -- individual component codes or the full composite sequence.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging import RangingCodeGenerator
|
||||||
|
|
||||||
|
gen = RangingCodeGenerator()
|
||||||
|
composite = gen.generate_sequence() # Full 5.4M chip code
|
||||||
|
a_code = gen.generate_a() # 31-chip A component
|
||||||
|
x_code = gen.generate_component("x") # By name
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `generate_cl` | `(n_chips: int \| None) -> np.ndarray` | Generate CL component: alternating 0, 1 clock. Default length: 2 |
|
||||||
|
| `generate_x` | `(n_chips: int \| None) -> np.ndarray` | Generate X component: 11-chip non-LFSR sequence with custom feedback logic |
|
||||||
|
| `generate_a` | `(n_chips: int \| None) -> np.ndarray` | Generate A component: 31-chip LFSR, 5 bits, taps [2,0] (x^5+x^2+1) |
|
||||||
|
| `generate_b` | `(n_chips: int \| None) -> np.ndarray` | Generate B component: 63-chip LFSR, 6 bits, taps [1,0] (x^6+x+1) |
|
||||||
|
| `generate_c` | `(n_chips: int \| None) -> np.ndarray` | Generate C component: 127-chip LFSR, 7 bits, taps [1,0] (x^7+x+1) |
|
||||||
|
| `generate_component` | `(name: str, n_chips: int \| None) -> np.ndarray` | Generate a named component (`"cl"`, `"x"`, `"a"`, `"b"`, `"c"`). Raises `ValueError` if name is not recognized |
|
||||||
|
| `generate_sequence` | `(n_chips: int \| None) -> np.ndarray` | Generate the full composite PRN code. Default: one full period (5,456,682 chips) |
|
||||||
|
|
||||||
|
All methods return `uint8` numpy arrays of 0/1 values. When `n_chips` is `None`, generates one full period for that component.
|
||||||
|
|
||||||
|
#### Component Code Properties
|
||||||
|
|
||||||
|
| Component | Length | Register | Taps | Init | Type |
|
||||||
|
|-----------|--------|----------|------|------|------|
|
||||||
|
| CL | 2 | -- | -- | -- | Alternating clock |
|
||||||
|
| X | 11 | 5-bit | Custom feedback | `0b10110` (22) | Non-LFSR |
|
||||||
|
| A | 31 | 5-bit | [2, 0] | `0x1F` (all ones) | Maximal-length LFSR |
|
||||||
|
| B | 63 | 6-bit | [1, 0] | `0x3F` (all ones) | Maximal-length LFSR |
|
||||||
|
| C | 127 | 7-bit | [1, 0] | `0x7F` (all ones) | Maximal-length LFSR |
|
||||||
|
|
||||||
|
#### Composite Code Generation
|
||||||
|
|
||||||
|
The composite code reproduces Shirriff's `calc()` algorithm: on even output chips (ck=0), all shift registers advance and the output is computed from feedback bits via majority logic. On odd chips (ck=1), the output is flipped.
|
||||||
|
|
||||||
|
Combination logic per step: `out = (NOT(xnew) AND maj(anew, bnew, cnew)) XOR ck`
|
||||||
|
|
||||||
|
where `maj(A,B,C) = (A&B) | (A&C) | (B&C)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `RangingCorrelator`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging`
|
||||||
|
**Type:** Pure-Python class
|
||||||
|
**Purpose:** Cross-correlate received NRZ samples with the known PRN code using FFT-based correlation for range measurement.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging import RangingCorrelator
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
correlator = RangingCorrelator(sample_rate=5_120_000, two_way=True)
|
||||||
|
result = correlator.correlate(received_samples)
|
||||||
|
print(f"Range: {result['range_m']:.1f} m")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `chip_rate` | `int` | `993963` | PRN chip rate in chips/sec (`RANGING_CHIP_RATE_HZ`) |
|
||||||
|
| `sample_rate` | `float` | `993963` | Input sample rate in Hz. When equal to `chip_rate`, each chip is one sample |
|
||||||
|
| `two_way` | `bool` | `True` | If `True`, range is halved (signal traveled ground to spacecraft and back) |
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `correlate` | `(received: np.ndarray, code_chips: int \| None) -> dict` | Cross-correlate received float samples with PRN reference. Returns measurement dict |
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `chip_rate` | `int` | PRN chip rate in chips/sec |
|
||||||
|
| `sample_rate` | `float` | Input sample rate in Hz |
|
||||||
|
| `two_way` | `bool` | Two-way range mode |
|
||||||
|
| `samples_per_chip` | `float` | Ratio `sample_rate / chip_rate` |
|
||||||
|
|
||||||
|
#### `correlate` Return Value
|
||||||
|
|
||||||
|
| Key | Type | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `delay_samples` | `int` | Peak correlation index in samples |
|
||||||
|
| `delay_chips` | `float` | Delay measured in chip periods |
|
||||||
|
| `range_m` | `float` | Computed range in meters (halved if `two_way=True`) |
|
||||||
|
| `correlation_peak` | `float` | Absolute value of the correlation peak |
|
||||||
|
| `peak_to_avg_ratio` | `float` | Peak divided by mean correlation magnitude. Higher values indicate cleaner detection |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `chips_to_range_m`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging`
|
||||||
|
**Type:** Pure-Python function
|
||||||
|
**Purpose:** Convert chip delay to range in meters.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging import chips_to_range_m
|
||||||
|
|
||||||
|
distance = chips_to_range_m(delay_chips=100.0, two_way=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
def chips_to_range_m(delay_chips: float, two_way: bool = True) -> float
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `delay_chips` | `float` | -- | Delay measured in chip periods |
|
||||||
|
| `two_way` | `bool` | `True` | If `True`, divide distance by 2 (round-trip signal path) |
|
||||||
|
|
||||||
|
#### Return Value
|
||||||
|
|
||||||
|
Range in meters (`float`). Uses `SPEED_OF_LIGHT_M_S` (299,792,458 m/s) and `RANGING_CHIP_RATE_HZ` (993,963 chip/s).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `range_m_to_chips`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging`
|
||||||
|
**Type:** Pure-Python function
|
||||||
|
**Purpose:** Convert range in meters to chip delay.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging import range_m_to_chips
|
||||||
|
|
||||||
|
chips = range_m_to_chips(range_m=384_400_000, two_way=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
def range_m_to_chips(range_m: float, two_way: bool = True) -> float
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `range_m` | `float` | -- | Distance in meters |
|
||||||
|
| `two_way` | `bool` | `True` | If `True`, compute round-trip delay (distance is doubled before conversion) |
|
||||||
|
|
||||||
|
#### Return Value
|
||||||
|
|
||||||
|
Delay in chip periods (`float`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `verify_code_properties`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging`
|
||||||
|
**Type:** Pure-Python function
|
||||||
|
**Purpose:** Self-test for code correctness -- verify all component codes have correct length, periodicity, and balance properties.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging import verify_code_properties
|
||||||
|
|
||||||
|
results = verify_code_properties()
|
||||||
|
for name, props in results.items():
|
||||||
|
print(f"{name}: {props}")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
def verify_code_properties() -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
#### Return Value
|
||||||
|
|
||||||
|
Dict with component names as keys (`"cl"`, `"x"`, `"a"`, `"b"`, `"c"`, `"length_product"`, `"composite_sample"`) and property dicts as values.
|
||||||
|
|
||||||
|
**Per-component dict:**
|
||||||
|
|
||||||
|
| Key | Type | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `length` | `int` | Actual generated length |
|
||||||
|
| `length_correct` | `bool` | Whether length matches the expected constant |
|
||||||
|
| `ones_count` | `int` | Number of 1-chips in the sequence |
|
||||||
|
| `zeros_count` | `int` | Number of 0-chips in the sequence |
|
||||||
|
| `periodic` | `bool` | Whether 2x generation repeats exactly |
|
||||||
|
| `balance_correct` | `bool` | Whether ones/zeros ratio matches LFSR theory (for A, B, C, CL) |
|
||||||
|
|
||||||
|
**`length_product` dict:**
|
||||||
|
|
||||||
|
| Key | Type | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `expected` | `int` | Product of all component lengths (2 x 11 x 31 x 63 x 127 = 5,456,682) |
|
||||||
|
| `matches_constant` | `bool` | Whether `RANGING_CODE_LENGTH` equals the product |
|
||||||
|
|
||||||
|
**`composite_sample` dict:**
|
||||||
|
|
||||||
|
| Key | Type | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `length` | `int` | Length of the test sample (10,000 chips) |
|
||||||
|
| `ones_count` | `int` | Number of 1-chips |
|
||||||
|
| `zeros_count` | `int` | Number of 0-chips |
|
||||||
|
| `balance` | `float` | Ratio of ones to total (near 0.5 for a balanced code) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ranging_source`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging_source`
|
||||||
|
**Type:** `gr.sync_block`
|
||||||
|
**Purpose:** GNU Radio source block producing a continuous PRN ranging chip stream. Pre-generates the full code period and cycles through it.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging_source import ranging_source
|
||||||
|
|
||||||
|
blk = ranging_source()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I/O Signature
|
||||||
|
|
||||||
|
| Port | Direction | Type | Description |
|
||||||
|
|------|-----------|------|-------------|
|
||||||
|
| (none) | Input | (none) | Source block -- no streaming input |
|
||||||
|
| `out0` | Output | `byte` | PRN chip stream (values 0 or 1), repeating every 5,456,682 chips |
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
None. The code period and chip rate are determined by the ranging constants.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The full 5,456,682-chip code is pre-generated at construction time and stored in memory (~5.2 MB). Streaming is zero-allocation after startup.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ranging_mod`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging_mod`
|
||||||
|
**Type:** `gr.hier_block2`
|
||||||
|
**Purpose:** NRZ-encode PRN chips at the baseband sample rate. Converts chip stream (bytes 0/1) to float NRZ waveform (+1/-1) suitable for summing with other subcarriers before PM modulation.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging_mod import ranging_mod
|
||||||
|
|
||||||
|
blk = ranging_mod(chip_rate=993_963, sample_rate=5_120_000)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I/O Signature
|
||||||
|
|
||||||
|
| Port | Direction | Type | Description |
|
||||||
|
|------|-----------|------|-------------|
|
||||||
|
| `in0` | Input | `byte` | Chip stream from `ranging_source` (values 0 or 1) |
|
||||||
|
| `out0` | Output | `float` | NRZ waveform (+1.0 / -1.0) at `sample_rate` |
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `chip_rate` | `int` | `993963` | PRN chip rate in chips/sec (`RANGING_CHIP_RATE_HZ`) |
|
||||||
|
| `sample_rate` | `float` | `5120000` | Output sample rate in Hz (`SAMPLE_RATE_BASEBAND`) |
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `samples_per_chip` | `int` | Samples per chip period: `int(sample_rate / chip_rate)` |
|
||||||
|
|
||||||
|
#### Internal Chain
|
||||||
|
|
||||||
|
`input -> char_to_float -> multiply_const_ff(2.0) -> add_const_ff(-1.0) -> repeat(samples_per_chip) -> output`
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
At 5.12 MHz sample rate with 993,963 chip/s, `samples_per_chip` is 5 (integer truncation of 5.1509...). This is functionally identical to `nrz_encoder` but configured for the ranging chip rate instead of the PCM bit rate.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ranging_demod`
|
||||||
|
|
||||||
|
**Module:** `apollo.ranging_demod`
|
||||||
|
**Type:** `gr.basic_block`
|
||||||
|
**Purpose:** FFT-based ranging demodulator. Accumulates samples in batches, cross-correlates with the known PRN code, and emits range measurement PDUs.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apollo.ranging_demod import ranging_demod
|
||||||
|
|
||||||
|
blk = ranging_demod(sample_rate=5_120_000, correlation_length=100_000)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I/O Signature
|
||||||
|
|
||||||
|
| Port | Direction | Type | Description |
|
||||||
|
|------|-----------|------|-------------|
|
||||||
|
| `in0` | Input | `float` (streaming) | PM demod output or filtered ranging signal |
|
||||||
|
| `"range"` | Output | Message (PDU) | Range measurement PDUs, one per correlation batch |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
This block has one streaming input and zero streaming outputs. All output is via the `"range"` message port.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
#### Constructor Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `chip_rate` | `int` | `993963` | PRN chip rate in chips/sec (`RANGING_CHIP_RATE_HZ`) |
|
||||||
|
| `sample_rate` | `float` | `5120000` | Input sample rate in Hz (`SAMPLE_RATE_BASEBAND`) |
|
||||||
|
| `correlation_length` | `int` | `100000` | Number of samples to accumulate per correlation batch. Longer batches improve SNR at the cost of measurement rate |
|
||||||
|
| `two_way` | `bool` | `True` | If `True`, range is halved (round-trip signal path) |
|
||||||
|
|
||||||
|
#### Output PDU Format
|
||||||
|
|
||||||
|
Each PDU is `pmt.cons(metadata, pmt.PMT_NIL)`:
|
||||||
|
|
||||||
|
**Metadata dict:**
|
||||||
|
| Key | PMT Type | Description |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| `"delay_chips"` | `pmt.from_double` | Measured delay in chip periods |
|
||||||
|
| `"range_m"` | `pmt.from_double` | Computed range in meters |
|
||||||
|
| `"correlation_peak"` | `pmt.from_double` | Absolute value of the correlation peak |
|
||||||
|
| `"peak_to_avg_ratio"` | `pmt.from_double` | Peak-to-average ratio (detection quality metric) |
|
||||||
|
| `"timestamp"` | `pmt.from_double` | `time.time()` when the measurement was emitted |
|
||||||
|
|||||||
@ -18,7 +18,12 @@ from apollo.downlink_decoder import DownlinkEngine as DownlinkEngine
|
|||||||
from apollo.pcm_demux import DemuxEngine as DemuxEngine
|
from apollo.pcm_demux import DemuxEngine as DemuxEngine
|
||||||
from apollo.pcm_frame_source import FrameSourceEngine as FrameSourceEngine
|
from apollo.pcm_frame_source import FrameSourceEngine as FrameSourceEngine
|
||||||
from apollo.pcm_frame_sync import FrameSyncEngine as FrameSyncEngine
|
from apollo.pcm_frame_sync import FrameSyncEngine as FrameSyncEngine
|
||||||
|
from apollo.ranging import RangingCodeGenerator as RangingCodeGenerator
|
||||||
|
from apollo.ranging import chips_to_range_m as chips_to_range_m
|
||||||
|
from apollo.ranging import range_m_to_chips as range_m_to_chips
|
||||||
from apollo.uplink_encoder import UplinkEncoder as UplinkEncoder
|
from apollo.uplink_encoder import UplinkEncoder as UplinkEncoder
|
||||||
|
from apollo.uplink_word_codec import UplinkDeserializerEngine as UplinkDeserializerEngine
|
||||||
|
from apollo.uplink_word_codec import UplinkSerializerEngine as UplinkSerializerEngine
|
||||||
from apollo.usb_signal_gen import generate_usb_baseband as generate_usb_baseband
|
from apollo.usb_signal_gen import generate_usb_baseband as generate_usb_baseband
|
||||||
|
|
||||||
# GNU Radio receive-side blocks (require gnuradio runtime)
|
# GNU Radio receive-side blocks (require gnuradio runtime)
|
||||||
@ -30,6 +35,7 @@ try:
|
|||||||
from apollo.pm_demod import pm_demod as pm_demod
|
from apollo.pm_demod import pm_demod as pm_demod
|
||||||
from apollo.sco_demod import sco_demod as sco_demod
|
from apollo.sco_demod import sco_demod as sco_demod
|
||||||
from apollo.subcarrier_extract import subcarrier_extract as subcarrier_extract
|
from apollo.subcarrier_extract import subcarrier_extract as subcarrier_extract
|
||||||
|
from apollo.uplink_word_codec import uplink_word_deserializer as uplink_word_deserializer
|
||||||
from apollo.voice_subcarrier_demod import voice_subcarrier_demod as voice_subcarrier_demod
|
from apollo.voice_subcarrier_demod import voice_subcarrier_demod as voice_subcarrier_demod
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass # GNU Radio not available — receive-side GR blocks won't be importable
|
pass # GNU Radio not available — receive-side GR blocks won't be importable
|
||||||
@ -42,7 +48,10 @@ try:
|
|||||||
from apollo.nrz_encoder import nrz_encoder as nrz_encoder
|
from apollo.nrz_encoder import nrz_encoder as nrz_encoder
|
||||||
from apollo.pcm_frame_source import pcm_frame_source as pcm_frame_source
|
from apollo.pcm_frame_source import pcm_frame_source as pcm_frame_source
|
||||||
from apollo.pm_mod import pm_mod as pm_mod
|
from apollo.pm_mod import pm_mod as pm_mod
|
||||||
|
from apollo.ranging_mod import ranging_mod as ranging_mod
|
||||||
|
from apollo.ranging_source import ranging_source as ranging_source
|
||||||
from apollo.sco_mod import sco_mod as sco_mod
|
from apollo.sco_mod import sco_mod as sco_mod
|
||||||
|
from apollo.uplink_word_codec import uplink_word_serializer as uplink_word_serializer
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass # GNU Radio not available — transmit-side GR blocks won't be importable
|
pass # GNU Radio not available — transmit-side GR blocks won't be importable
|
||||||
|
|
||||||
@ -54,8 +63,11 @@ try:
|
|||||||
from apollo.fm_signal_source import fm_signal_source as fm_signal_source
|
from apollo.fm_signal_source import fm_signal_source as fm_signal_source
|
||||||
from apollo.pcm_demux import pcm_demux as pcm_demux
|
from apollo.pcm_demux import pcm_demux as pcm_demux
|
||||||
from apollo.pcm_frame_sync import pcm_frame_sync as pcm_frame_sync
|
from apollo.pcm_frame_sync import pcm_frame_sync as pcm_frame_sync
|
||||||
|
from apollo.ranging_demod import ranging_demod as ranging_demod
|
||||||
from apollo.uplink_encoder import uplink_encoder as uplink_encoder
|
from apollo.uplink_encoder import uplink_encoder as uplink_encoder
|
||||||
from apollo.usb_downlink_receiver import usb_downlink_receiver as usb_downlink_receiver
|
from apollo.usb_downlink_receiver import usb_downlink_receiver as usb_downlink_receiver
|
||||||
from apollo.usb_signal_source import usb_signal_source as usb_signal_source
|
from apollo.usb_signal_source import usb_signal_source as usb_signal_source
|
||||||
|
from apollo.usb_uplink_receiver import usb_uplink_receiver as usb_uplink_receiver
|
||||||
|
from apollo.usb_uplink_source import usb_uplink_source as usb_uplink_source
|
||||||
except (ImportError, NameError):
|
except (ImportError, NameError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -174,3 +174,36 @@ TX_IMPEDANCE_OHM = 50
|
|||||||
TWT_LOW_POWER_W = 5
|
TWT_LOW_POWER_W = 5
|
||||||
TWT_HIGH_POWER_W = 20
|
TWT_HIGH_POWER_W = 20
|
||||||
TWT_WARMUP_S = 90
|
TWT_WARMUP_S = 90
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Uplink Modulation Parameters (IMPL_SPEC section 2.2)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
UPLINK_PM_DEVIATION_RAD = 1.0 # 1.0 rad peak phase deviation
|
||||||
|
UPLINK_DATA_BIT_RATE = 2_000 # 2 kbps NRZ
|
||||||
|
UPLINK_DATA_FM_DEVIATION_HZ = 4_000 # ±4 kHz on 70 kHz subcarrier
|
||||||
|
UPLINK_VOICE_FM_DEVIATION_HZ = 7_500 # ±7.5 kHz on 30 kHz subcarrier
|
||||||
|
UPLINK_WORD_BITS = 15 # AGC word width
|
||||||
|
UPLINK_INTER_WORD_GAP = 3 # bit periods of zeros between words
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PRN Ranging (Ken Shirriff / NASA docs)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RANGING_CHIP_RATE_HZ = 993_963 # ~994 kchip/s
|
||||||
|
RANGING_CODE_LENGTH = 5_456_682 # 2 × 11 × 31 × 63 × 127
|
||||||
|
RANGING_CL_LENGTH = 2
|
||||||
|
RANGING_X_LENGTH = 11
|
||||||
|
RANGING_A_LENGTH = 31
|
||||||
|
RANGING_B_LENGTH = 63
|
||||||
|
RANGING_C_LENGTH = 127
|
||||||
|
|
||||||
|
RANGING_X_INIT = 22 # 0b10110
|
||||||
|
RANGING_A_INIT = 0x1F # all ones (5-bit)
|
||||||
|
RANGING_B_INIT = 0x3F # all ones (6-bit)
|
||||||
|
RANGING_C_INIT = 0x7F # all ones (7-bit)
|
||||||
|
|
||||||
|
# LFSR taps: zero-indexed bit positions for XOR feedback (Shirriff's Teensy code)
|
||||||
|
RANGING_A_TAPS = (2, 0) # 5-bit: x^5 + x^2 + 1
|
||||||
|
RANGING_B_TAPS = (1, 0) # 6-bit: x^6 + x + 1
|
||||||
|
RANGING_C_TAPS = (1, 0) # 7-bit: x^7 + x + 1
|
||||||
|
|
||||||
|
SPEED_OF_LIGHT_M_S = 299_792_458
|
||||||
|
|||||||
@ -28,29 +28,23 @@ Reference: Ken Shirriff's analysis of the Apollo ranging system
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
from apollo.constants import (
|
||||||
# Ranging constants (local to this module; will migrate to constants.py)
|
RANGING_A_INIT,
|
||||||
# ---------------------------------------------------------------------------
|
RANGING_A_LENGTH,
|
||||||
RANGING_CHIP_RATE_HZ = 993_963
|
RANGING_A_TAPS,
|
||||||
RANGING_CODE_LENGTH = 5_456_682 # 2 * 11 * 31 * 63 * 127
|
RANGING_B_INIT,
|
||||||
RANGING_CL_LENGTH = 2
|
RANGING_B_LENGTH,
|
||||||
RANGING_X_LENGTH = 11
|
RANGING_B_TAPS,
|
||||||
RANGING_A_LENGTH = 31
|
RANGING_C_INIT,
|
||||||
RANGING_B_LENGTH = 63
|
RANGING_C_LENGTH,
|
||||||
RANGING_C_LENGTH = 127
|
RANGING_C_TAPS,
|
||||||
|
RANGING_CHIP_RATE_HZ,
|
||||||
RANGING_X_INIT = 22 # 0b10110
|
RANGING_CL_LENGTH,
|
||||||
RANGING_A_INIT = 0x1F # all ones (5-bit)
|
RANGING_CODE_LENGTH,
|
||||||
RANGING_B_INIT = 0x3F # all ones (6-bit)
|
RANGING_X_INIT,
|
||||||
RANGING_C_INIT = 0x7F # all ones (7-bit)
|
RANGING_X_LENGTH,
|
||||||
|
SPEED_OF_LIGHT_M_S,
|
||||||
# LFSR taps: zero-indexed bit positions for XOR feedback, per Shirriff's code.
|
)
|
||||||
# These produce maximal-length sequences (period = 2^n - 1).
|
|
||||||
RANGING_A_TAPS = (2, 0) # 5-bit: x^5 + x^2 + 1
|
|
||||||
RANGING_B_TAPS = (1, 0) # 6-bit: x^6 + x + 1
|
|
||||||
RANGING_C_TAPS = (1, 0) # 7-bit: x^7 + x + 1
|
|
||||||
|
|
||||||
SPEED_OF_LIGHT_M_S = 299_792_458
|
|
||||||
|
|
||||||
|
|
||||||
class RangingCodeGenerator:
|
class RangingCodeGenerator:
|
||||||
|
|||||||
@ -17,13 +17,12 @@ import time
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from apollo.constants import SAMPLE_RATE_BASEBAND
|
||||||
from apollo.ranging import (
|
from apollo.ranging import (
|
||||||
RANGING_CHIP_RATE_HZ,
|
RANGING_CHIP_RATE_HZ,
|
||||||
RangingCorrelator,
|
RangingCorrelator,
|
||||||
)
|
)
|
||||||
|
|
||||||
SAMPLE_RATE_BASEBAND = 5_120_000
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# GNU Radio block (optional -- only if gnuradio is available)
|
# GNU Radio block (optional -- only if gnuradio is available)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -16,11 +16,9 @@ Reference: IMPLEMENTATION_SPEC.md ranging modulation path
|
|||||||
|
|
||||||
from gnuradio import blocks, gr
|
from gnuradio import blocks, gr
|
||||||
|
|
||||||
|
from apollo.constants import SAMPLE_RATE_BASEBAND
|
||||||
from apollo.ranging import RANGING_CHIP_RATE_HZ
|
from apollo.ranging import RANGING_CHIP_RATE_HZ
|
||||||
|
|
||||||
# Use the standard baseband sample rate
|
|
||||||
SAMPLE_RATE_BASEBAND = 5_120_000
|
|
||||||
|
|
||||||
|
|
||||||
class ranging_mod(gr.hier_block2):
|
class ranging_mod(gr.hier_block2):
|
||||||
"""Ranging chip NRZ modulator: byte (0/1) -> float (+1/-1) at sample_rate.
|
"""Ranging chip NRZ modulator: byte (0/1) -> float (+1/-1) at sample_rate.
|
||||||
|
|||||||
@ -18,15 +18,15 @@ from collections import deque
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from apollo.constants import AGC_CH_INLINK
|
from apollo.constants import (
|
||||||
|
AGC_CH_INLINK,
|
||||||
|
UPLINK_DATA_BIT_RATE,
|
||||||
|
UPLINK_INTER_WORD_GAP,
|
||||||
|
UPLINK_WORD_BITS,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Uplink parameters (defined locally per integration instructions)
|
|
||||||
UPLINK_DATA_BIT_RATE = 2_000 # 2 kbps NRZ
|
|
||||||
UPLINK_WORD_BITS = 15 # AGC word width
|
|
||||||
UPLINK_INTER_WORD_GAP = 3 # bit periods of zeros between words
|
|
||||||
|
|
||||||
# Minimum consecutive zeros to consider the channel idle (for deserializer sync)
|
# Minimum consecutive zeros to consider the channel idle (for deserializer sync)
|
||||||
IDLE_THRESHOLD = UPLINK_INTER_WORD_GAP + 2
|
IDLE_THRESHOLD = UPLINK_INTER_WORD_GAP + 2
|
||||||
|
|
||||||
|
|||||||
@ -20,16 +20,14 @@ from gnuradio import analog, blocks, digital, filter, gr
|
|||||||
|
|
||||||
from apollo.constants import (
|
from apollo.constants import (
|
||||||
SAMPLE_RATE_BASEBAND,
|
SAMPLE_RATE_BASEBAND,
|
||||||
|
UPLINK_DATA_BIT_RATE,
|
||||||
|
UPLINK_DATA_FM_DEVIATION_HZ,
|
||||||
UPLINK_DATA_SUBCARRIER_HZ,
|
UPLINK_DATA_SUBCARRIER_HZ,
|
||||||
)
|
)
|
||||||
from apollo.pm_demod import pm_demod
|
from apollo.pm_demod import pm_demod
|
||||||
from apollo.subcarrier_extract import subcarrier_extract
|
from apollo.subcarrier_extract import subcarrier_extract
|
||||||
from apollo.uplink_word_codec import uplink_word_deserializer
|
from apollo.uplink_word_codec import uplink_word_deserializer
|
||||||
|
|
||||||
# Uplink parameters (defined locally per integration instructions)
|
|
||||||
UPLINK_DATA_BIT_RATE = 2_000
|
|
||||||
UPLINK_DATA_FM_DEVIATION_HZ = 4_000
|
|
||||||
|
|
||||||
|
|
||||||
class usb_uplink_receiver(gr.hier_block2):
|
class usb_uplink_receiver(gr.hier_block2):
|
||||||
"""Apollo USB uplink receiver -- complex baseband to command PDUs.
|
"""Apollo USB uplink receiver -- complex baseband to command PDUs.
|
||||||
|
|||||||
@ -22,17 +22,15 @@ from gnuradio import analog, blocks, gr
|
|||||||
|
|
||||||
from apollo.constants import (
|
from apollo.constants import (
|
||||||
SAMPLE_RATE_BASEBAND,
|
SAMPLE_RATE_BASEBAND,
|
||||||
|
UPLINK_DATA_BIT_RATE,
|
||||||
|
UPLINK_DATA_FM_DEVIATION_HZ,
|
||||||
UPLINK_DATA_SUBCARRIER_HZ,
|
UPLINK_DATA_SUBCARRIER_HZ,
|
||||||
|
UPLINK_PM_DEVIATION_RAD,
|
||||||
)
|
)
|
||||||
from apollo.nrz_encoder import nrz_encoder
|
from apollo.nrz_encoder import nrz_encoder
|
||||||
from apollo.pm_mod import pm_mod
|
from apollo.pm_mod import pm_mod
|
||||||
from apollo.uplink_word_codec import uplink_word_serializer
|
from apollo.uplink_word_codec import uplink_word_serializer
|
||||||
|
|
||||||
# Uplink parameters (defined locally per integration instructions)
|
|
||||||
UPLINK_PM_DEVIATION_RAD = 1.0
|
|
||||||
UPLINK_DATA_BIT_RATE = 2_000
|
|
||||||
UPLINK_DATA_FM_DEVIATION_HZ = 4_000
|
|
||||||
|
|
||||||
|
|
||||||
class usb_uplink_source(gr.hier_block2):
|
class usb_uplink_source(gr.hier_block2):
|
||||||
"""Apollo USB uplink signal source -- complex baseband output.
|
"""Apollo USB uplink signal source -- complex baseband output.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user