Add Phase 1 experimenter tools: MCP server, H21cm, beacon logger, arc survey

Four new tools transforming the SkyWalker-1 from satellite TV receiver into
a general-purpose RF observatory:

- skywalker-mcp: FastMCP server exposing 20 tools, 4 resources, 2 prompts.
  Thread-safe DeviceBridge with motor safety (continuous drive opt-in),
  input validation on all frequency/symbol rate/step parameters,
  try/finally on TS capture, path traversal sanitization, and reduced
  lock scope so emergency motor halt isn't blocked during long surveys.

- h21cm.py: Hydrogen 21 cm drift-scan radiometer at 1420.405 MHz with
  Doppler velocity calculation, control band comparison, and CSV output.

- beacon_logger.py: Long-term Ku-band beacon SNR/AGC logger with auto-relock,
  dual CSV/JSONL output, signal handlers, and systemd unit generation.

- arc_survey.py: Multi-satellite orbital arc census with USALS motor control,
  per-slot catalog persistence, resume support, and defensive motor halt
  on all error/interrupt paths.

Documentation: experimenter's roadmap guide + 4 tool reference pages (48 pages total).
This commit is contained in:
Ryan Malloy 2026-02-17 14:45:02 -07:00
parent 6c00f941eb
commit a9dcf84c38
15 changed files with 4374 additions and 0 deletions

View file

@ -0,0 +1,218 @@
---
title: Experimenter's Roadmap
description: What else can the SkyWalker-1 hardware do? A field guide to creative RF experiments beyond satellite TV reception.
---
import { Aside, Badge, Card, CardGrid, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
The SkyWalker-1 reverse engineering project has produced a fully documented, custom-firmware-driven,
Python-controllable RF instrument. With v3.05.0 deployed and all 55 Hamilton safety tests passing,
the question becomes: **what can experimenters actually do with this platform beyond satellite TV reception?**
The approach: start from the hardware capabilities, ask "what physical processes produce or interact
with signals these capabilities can measure," and reason outward to creative applications.
## Hardware as a Platform
The SkyWalker-1 is simultaneously:
- A **power meter** (16-bit AGC, ~30-40 dB dynamic range)
- A **spectrum analyzer** (~346 kHz RBW, 950-2150 MHz)
- A **satellite receiver** (10 modulation types, 256 ksps - 30 Msps)
- A **time-series data logger** (signal_monitor at ~50 Hz)
- A **dish positioning system** (DiSEqC 1.2 motor, USALS GotoX)
All controllable from Python.
<Aside type="tip" title="The key realization">
The AGC registers respond to **any RF energy** at the tuned frequency, regardless of modulation.
You don't need to demodulate a signal to detect and measure it.
</Aside>
## What's Directly Receivable (No LNB)
The 950-2150 MHz IF range contains far more than satellite TV when you connect an antenna directly:
| Frequency | What's There | Detectable? |
|---|---|---|
| **1420.405 MHz** | Hydrogen 21 cm line — galactic emission | Yes (AGC power) |
| 1575.42 MHz | GPS L1 | Yes (energy) |
| 1176.45 MHz | GPS L5 / Galileo E5a | Yes (energy) |
| 1227.6 MHz | GPS L2 | Yes (energy) |
| 1602 MHz | GLONASS L1 | Yes (energy) |
| 1525-1559 MHz | Inmarsat downlink | Yes (energy) |
| 1616-1626 MHz | Iridium downlink | Yes (burst energy) |
| 1670-1710 MHz | GOES LRIT, NOAA HRPT | Yes (carrier) |
| 1240-1300 MHz | Amateur 23 cm band | Yes (energy) |
## Phase 1 Tools (Available Now)
### Hydrogen 21 cm Drift-Scan Radiometer
<Badge text="tools/h21cm.py" variant="note" />
Neutral hydrogen emits at 1420.405 MHz — directly in the IF range. The Milky Way's spiral arms
create a velocity-dispersed emission profile detectable even with the BCM4500's resolution bandwidth.
<Steps>
1. Connect an L-band antenna (patch, helical, or horn) directly to the F-connector
2. Run `python h21cm.py` for a single sweep centered on 1420.405 MHz
3. Look for elevated power across the 1419-1421 MHz range (the hydrogen emission)
4. Use `--drift --duration 3600` for a one-hour drift scan as Earth rotates
5. Use `--averages 8` for 8x averaging to pull the signal from the noise
</Steps>
```bash
# Single sweep with 8x averaging
python tools/h21cm.py --averages 8
# One-hour drift scan with CSV output
python tools/h21cm.py --drift --duration 3600 --averages 4 --output h21cm-data.csv
# Include control band comparison
python tools/h21cm.py --averages 8 --control
```
The velocity of the detected hydrogen is calculated from Doppler shift:
**v = c × (f_rest f_observed) / f_rest**. This maps directly to the rotation curve
of the Milky Way.
### Beacon Logger
<Badge text="tools/beacon_logger.py" variant="note" />
Lock onto a stable Ku-band broadcast transponder and log SNR/AGC at configurable intervals
for hours, days, or weeks. Produces propagation datasets useful for:
- **Rain fade analysis** — correlate attenuation with rainfall rate
- **Diurnal thermal drift** — track LNB gain vs. temperature over 24 hours
- **Antenna mount stability** — detect dish drift from wind/thermal expansion
- **ITU propagation model validation** — contribute real measurements
```bash
# Log a Ku-band beacon for 24 hours, 1 sample/sec, report every minute
python tools/beacon_logger.py --freq 1265000 --sr 20000000 \
--output beacon-24h.csv --json-output beacon-stats.jsonl \
--duration 86400
# Generate a systemd unit file for unattended operation
python tools/beacon_logger.py --generate-systemd \
--freq 1265000 --sr 20000000 --output /var/log/beacon.csv
```
The logger automatically re-locks on signal loss and computes per-interval statistics
(min/max/mean/stddev of SNR).
### Multi-Satellite Arc Survey
<Badge text="tools/arc_survey.py" variant="note" />
Automated "satellite census": points the dish motor to each GEO orbital longitude,
runs a full-band six-stage survey at each position, and aggregates results into a comprehensive sky map.
```bash
# Survey specific North American slots
python tools/arc_survey.py --observer-lon -96.8 --slots "97W,99W,101W,103W"
# Survey an arc at 3-degree intervals
python tools/arc_survey.py --observer-lon -96.8 --arc -120 -60 --step 3
# List common NA orbital slots
python tools/arc_survey.py --list-slots
# Resume an interrupted survey
python tools/arc_survey.py --resume ~/.skywalker1/arc-surveys/arc-survey-2026-02-17.json
```
<Aside type="caution" title="Time commitment">
Each orbital slot takes 5-15 minutes to survey. A full North American arc
(~35 positions) is an overnight operation. The tool saves progress after each
slot, so Ctrl-C pauses safely and `--resume` continues where you left off.
</Aside>
### MCP Server
<Badge text="mcp/skywalker-mcp/" variant="note" />
The `skywalker-mcp` FastMCP server wraps the entire hardware API as MCP tools, making
every function accessible to LLMs. This is the foundation for autonomous RF exploration.
```bash
# Install and run (from project directory)
cd mcp/skywalker-mcp && uv run skywalker-mcp
# Add to Claude Code
claude mcp add skywalker-mcp -- uv run --directory mcp/skywalker-mcp skywalker-mcp
# Test with headless mode
claude -p "What firmware version is loaded?" \
--mcp-config .mcp.json --allowedTools "mcp__skywalker-mcp__*"
```
**20 MCP tools** covering:
- Device status and signal quality
- Spectrum sweep with peak detection
- Frequency tuning across 10 modulation types
- Blind scan for unknown carriers
- Six-stage carrier survey with catalog persistence
- Dish motor control (jog, goto, USALS)
- LNB configuration
- I2C bus scanning
- Transport stream capture and PSI parsing
- Frequency identification against allocation tables
**MCP Resources:**
- `skywalker://status` — live device state
- `skywalker://catalog/latest` — most recent survey
- `skywalker://allocations/lband` — frequency allocation table
- `skywalker://modulations` — supported modulation types
**MCP Prompts:**
- `explore_rf_environment` — autonomous RF exploration strategy
- `hydrogen_line_observation` — guided hydrogen 21 cm procedure
## Future Tiers
### Tier B: Creative Combinations (no firmware changes)
| Idea | Description |
|---|---|
| **Gradient-descent dish auto-peaking** | Closed-loop: tune to beacon, read AGC, step motor, converge |
| **Rain fade monitor** | Dual-frequency SNR logging + weather API correlation |
| **NIT-driven survey** | Parse NIT from one carrier → skip to ALL transponders |
| **Spectral anomaly detection** | Build baseline, flag N-sigma deviations, catch interference |
| **PCR timing analysis** | Extract clock jitter from transport stream timestamps |
### Tier C: Firmware Enhancements (v4.x)
| Feature | Impact | Size |
|---|---|---|
| Continuous AGC streaming via EP2 | Real-time waterfall displays | ~100 bytes |
| Firmware-side moving average | 12 dB noise floor improvement (16x avg) | ~50 bytes |
| GPIO event counter via Timer1 | General-purpose frequency counter | ~80 bytes |
| Multi-byte I2C transactions | External sensor integration | ~120 bytes |
| Fast power-only read (no retune) | 2x measurement rate | ~30 bytes |
### Tier D: External Hardware
- **I2C environmental sensors** (BME280/SHT40) for LNB drift vs. temperature
- **External TCXO/OCXO reference** for calibrated LNB frequency (±50 Hz vs ±5 MHz)
- **Noise source + Y-factor calibration** for absolute power measurements
- **GPIO antenna switch matrix** for automated antenna comparison
## Who Gets What
<CardGrid>
<Card title="RF Phreaks" icon="rocket">
Iridium burst detector, arc survey, anomaly detection, MCP autonomous explorer
</Card>
<Card title="Ham Radio" icon="open-book">
Hydrogen line, dish auto-peaking, QO-100 monitoring, arc survey
</Card>
<Card title="Skywatchers" icon="star">
Hydrogen line, GNSS monitoring, arc survey, beacon logger
</Card>
<Card title="Engineers" icon="setting">
Beacon logger, rain fade monitor, polarization analysis, PCR timing
</Card>
</CardGrid>

View file

@ -0,0 +1,95 @@
---
title: Arc Survey
description: Automated multi-satellite orbital arc survey with motor control and carrier catalog aggregation.
---
import { Aside, Steps } from '@astrojs/starlight/components';
The `arc_survey.py` tool automates a complete satellite census across the GEO orbital arc.
It points the dish motor to each orbital longitude, runs a full six-stage carrier survey,
saves individual catalogs, and produces an aggregated sky map.
## Quick Start
```bash
# Survey 4 specific North American slots
python tools/arc_survey.py --observer-lon -96.8 --slots "97W,99W,101W,103W"
# Survey a 60-degree arc at 3-degree intervals
python tools/arc_survey.py --observer-lon -96.8 --arc -120 -60 --step 3
# List common NA orbital positions
python tools/arc_survey.py --list-slots
```
## How It Works
<Steps>
1. **Parse orbital slot list** from CLI, JSON file, or arc range
2. **For each slot**: send USALS GotoX command to motor
3. **Wait for settle** (scales with angular distance, minimum 15 seconds)
4. **Run six-stage survey**: coarse sweep → peak detection → fine sweep → blind scan → TS sample → catalog
5. **Save per-slot catalog** to `~/.skywalker1/surveys/`
6. **Save arc survey state** to `~/.skywalker1/arc-surveys/` (for resume)
7. **Print aggregated summary** with per-slot carrier counts
</Steps>
## Resume Support
The tool saves state after every completed slot. If interrupted (Ctrl-C, power loss, etc.):
```bash
python tools/arc_survey.py --resume ~/.skywalker1/arc-surveys/arc-survey-2026-02-17.json
```
Already-surveyed slots are skipped. The survey continues from where it left off.
## Slot Specification
<Aside type="tip" title="Slot formats">
Slots accept multiple formats: `97W`, `97.5W`, `3E`, `-97`, `-97.5`
</Aside>
**CLI list:**
```bash
--slots "97W,99W,101W,103W,105W"
```
**JSON file:**
```json
[
{"name": "97W", "lon": -97.0},
{"name": "99W", "lon": -99.0},
{"name": "Galaxy 16", "lon": -99.0}
]
```
**Arc range:**
```bash
--arc -120 -60 --step 3
```
## Options
| Flag | Default | Description |
|---|---|---|
| `--observer-lon` | (required) | Observer longitude (negative = west) |
| `--observer-lat` | 0.0 | Observer latitude |
| `--slots` | — | Comma-separated slot list |
| `--file` | — | JSON file with slot definitions |
| `--arc` | — | Start/stop longitude for range scan |
| `--step` | 3.0 | Degrees between arc positions |
| `--coarse-step` | 5.0 | MHz step for coarse spectrum sweep |
| `--settle-time` | 15 | Minimum motor settle time (seconds) |
| `--resume` | — | Resume from saved state file |
| `--list-slots` | — | Print common NA slots and exit |
## Output Files
| Location | Content |
|---|---|
| `~/.skywalker1/surveys/arc-DATE-SLOT.json` | Per-slot carrier catalog |
| `~/.skywalker1/arc-surveys/arc-survey-DATE.json` | Arc survey state (for resume) |
The per-slot catalogs are standard `CarrierCatalog` JSON files, compatible with
the diff and comparison tools in `carrier_catalog.py`.

View file

@ -0,0 +1,75 @@
---
title: Beacon Logger
description: Long-term satellite signal logging for propagation research, rain fade analysis, and link budget validation.
---
import { Aside, Steps } from '@astrojs/starlight/components';
The `beacon_logger.py` tool locks onto a stable satellite transponder and records SNR, AGC levels,
and signal power at configurable intervals. Multi-day datasets reveal rain fade events, diurnal
thermal effects, antenna mount drift, and LNB gain variations.
## Quick Start
```bash
# Log a Ku-band transponder for 1 hour, CSV output
python tools/beacon_logger.py --freq 1265000 --sr 20000000 --output beacon.csv
# 24-hour unattended logging with per-minute statistics
python tools/beacon_logger.py --freq 1265000 --sr 20000000 \
--output beacon-24h.csv --json-output stats.jsonl --duration 86400
```
<Aside type="note" title="Frequency units">
The `--freq` parameter is in **kHz** (IF frequency). For a Ku-band transponder at 12015 MHz
with a universal LNB at LO 10750 MHz, the IF is 12015 10750 = 1265 MHz = **1265000 kHz**.
</Aside>
## Features
- **Auto-relock**: If signal is lost (weather, dish movement), automatically retunes and relocks
- **Statistics per interval**: min/max/mean/stddev of SNR over each reporting window
- **Dual output**: Raw per-sample CSV + per-interval JSONL statistics
- **Signal handlers**: Clean shutdown on SIGTERM/SIGINT, no data loss
- **Systemd integration**: `--generate-systemd` prints a ready-to-use unit file
## Options
| Flag | Default | Description |
|---|---|---|
| `--freq` | (required) | IF frequency in kHz |
| `--sr` | 20000000 | Symbol rate in sps |
| `--mod` | qpsk | Modulation type |
| `--fec` | auto | FEC rate |
| `--output` | — | CSV output file (raw samples) |
| `--json-output` | — | JSONL file (per-interval statistics) |
| `--duration` | 3600 | Logging duration in seconds |
| `--sample-interval` | 1.0 | Seconds between samples |
| `--report-interval` | 60 | Seconds between summary reports |
| `--pol` | — | LNB polarization (H/V) |
| `--band` | — | LNB band (low/high) |
| `--daemon` | — | Suppress stdout for background operation |
| `--generate-systemd` | — | Print systemd unit file and exit |
## Systemd Daemon Mode
```bash
# Generate and install the service
python tools/beacon_logger.py --generate-systemd \
--freq 1265000 --sr 20000000 \
--output /var/log/skywalker/beacon.csv \
--duration 999999 > /tmp/beacon-logger.service
sudo cp /tmp/beacon-logger.service /etc/systemd/system/
sudo systemctl enable --now beacon-logger
```
## Applications
| Use Case | What to Measure | Interval |
|---|---|---|
| Rain fade | SNR drops during precipitation | 1 Hz |
| LNB thermal drift | AGC shift over temperature cycle | 10s |
| Antenna mount stability | Slow SNR decay over days | 60s |
| Link budget validation | Long-term average vs. predicted | 60s |
| Ionospheric scintillation | Rapid AGC fluctuations | 1 Hz |

View file

@ -0,0 +1,81 @@
---
title: Hydrogen 21 cm Radiometer
description: Detect neutral hydrogen emission at 1420.405 MHz using the SkyWalker-1 as an L-band radiometer.
---
import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
The `h21cm.py` tool turns the SkyWalker-1 into a hydrogen line radiometer. Neutral hydrogen
atoms emit radiation at **1420.405 MHz** when the electron's spin flips relative to the proton —
the most fundamental spectral line in radio astronomy, and it falls directly in the IF range.
No LNB is needed. Connect an L-band antenna directly to the F-connector.
## Quick Start
```bash
# Single sweep, 8x averaging for best sensitivity
python tools/h21cm.py --averages 8
# One-hour drift scan with CSV output
python tools/h21cm.py --drift --duration 3600 --averages 4 --output h21cm-data.csv
```
## How It Works
<Steps>
1. **LNB power is disabled** — direct input mode, no frequency conversion
2. **Sweeps 1418-1422 MHz** (configurable) at 0.5 MHz steps
3. **Measures AGC power** at each step — the BCM4500 responds to any RF energy
4. **Estimates baseline** from the band edges (where no hydrogen is expected)
5. **Calculates excess power** above baseline — the hydrogen emission
6. **Computes Doppler velocity** for each frequency bin
</Steps>
The velocity axis maps frequency to radial velocity via:
**v = c × (1420.405 f_observed) / 1420.405**
Positive velocity = hydrogen moving away (lower frequency). The ~200 km/s spread
in a typical observation maps the rotation curve of the Milky Way.
## Options
| Flag | Default | Description |
|---|---|---|
| `--center` | 1420.405 | Center frequency in MHz |
| `--span` | 4.0 | Frequency span in MHz |
| `--step` | 0.5 | Frequency step in MHz |
| `--dwell` | 50 | Integration time per step in ms |
| `--averages` | 1 | Number of sweeps to average (4-16 recommended) |
| `--output` | — | CSV output file |
| `--control` | — | Include control band comparison |
| `--drift` | — | Enable drift scan mode |
| `--duration` | 3600 | Drift scan duration in seconds |
| `--interval` | 60 | Seconds between drift scans |
| `--motor-step` | 0 | Motor steps between scans (declination scanning) |
## Sensitivity Notes
<Aside type="tip" title="Improving SNR">
The hydrogen line is weak. The BCM4500's ~346 kHz resolution bandwidth is actually
helpful here — it's wide enough to capture the broad galactic emission without
excessive noise. Use `--averages 8` or higher and `--dwell 100` for best results.
Each doubling of averages improves SNR by ~3 dB.
</Aside>
The `--control` flag sweeps an adjacent band (1430-1434 MHz) where no hydrogen emission
is expected. Comparing the two bands confirms that any detected bump is real signal,
not system noise variation.
## CSV Output Format
| Column | Description |
|---|---|
| `timestamp` | ISO 8601 UTC timestamp |
| `scan_num` | Scan number (drift mode only) |
| `freq_mhz` | Frequency in MHz |
| `power_db` | Raw power in dB (relative) |
| `excess_db` | Power above baseline |
| `velocity_km_s` | Doppler velocity in km/s |
| `baseline_db` | Estimated noise floor |

View file

@ -0,0 +1,112 @@
---
title: MCP Server
description: Model Context Protocol server that exposes the SkyWalker-1 hardware API to LLMs for autonomous RF exploration.
---
import { Aside, Steps } from '@astrojs/starlight/components';
The `skywalker-mcp` package wraps the entire SkyWalker-1 Python API as an MCP (Model Context Protocol)
server, making every hardware function accessible to LLMs. This enables natural-language signal analysis,
autonomous RF exploration, and scheduled observation campaigns.
## Installation
```bash
cd mcp/skywalker-mcp
uv sync
```
## Running
```bash
# Local development
uv run --directory mcp/skywalker-mcp skywalker-mcp
# Add to Claude Code
claude mcp add skywalker-mcp -- uv run --directory mcp/skywalker-mcp skywalker-mcp
```
## Tools (20)
### Device Status
| Tool | Description |
|---|---|
| `get_device_status` | Firmware version, config bits, USB speed, serial, last error |
| `get_signal_quality` | SNR, AGC, power, lock status |
| `get_stream_diagnostics` | Poll count, overflows, sync loss |
### Spectrum & Tuning
| Tool | Description |
|---|---|
| `sweep_spectrum` | Full-band power measurement with peak detection |
| `tune_frequency` | Tune to specific freq/modulation/FEC, read signal |
| `run_blind_scan` | Symbol rate sweep at single frequency |
### Survey & Catalog
| Tool | Description |
|---|---|
| `run_carrier_survey` | Six-stage pipeline: sweep → peaks → blind → TS → catalog |
| `compare_surveys` | Diff two saved catalogs for changes |
| `list_surveys` | List saved survey files with metadata |
### Dish Motor
| Tool | Description |
|---|---|
| `move_dish` | Halt, east, west, goto slot, USALS GotoX (continuous drive requires explicit opt-in) |
| `jog_dish` | Small steps (1-30) + signal quality readback |
| `store_position` | Save current position to memory slot |
### LNB & I2C
| Tool | Description |
|---|---|
| `set_lnb_config` | Voltage (13V/18V), 22 kHz tone, power off |
| `scan_i2c_bus` | Enumerate all I2C devices |
| `read_i2c_register` | Read single byte from I2C address |
### Transport Stream & Identification
| Tool | Description |
|---|---|
| `capture_transport_stream` | Capture + parse PAT/PMT/SDT for service names |
| `identify_frequency` | Look up frequency against allocation tables |
## Resources
| URI | Description |
|---|---|
| `skywalker://status` | Live device state (firmware, config, signal) |
| `skywalker://catalog/latest` | Most recent survey catalog as JSON |
| `skywalker://allocations/lband` | L-band frequency allocation table |
| `skywalker://modulations` | Supported modulations and FEC rates |
## Prompts
| Prompt | Description |
|---|---|
| `explore_rf_environment` | Strategy for autonomous RF discovery |
| `hydrogen_line_observation` | Guided 21 cm observation procedure |
## Architecture
<Aside type="note" title="Thread safety">
The BCM4500 demodulator cannot handle overlapping USB control transfers. The MCP server
uses the same `DeviceBridge` pattern as the TUI — a `threading.RLock` serializes all
hardware access. Async MCP handlers use `asyncio.to_thread()` to avoid blocking the
event loop during USB I/O.
</Aside>
The server uses FastMCP's lifespan pattern: the USB device opens on server startup and
closes on shutdown. All tools receive the device bridge through the lifespan context.
## Testing
```bash
# Verify the server starts and can talk to hardware
claude -p "What firmware version is loaded?" \
--mcp-config .mcp.json \
--allowedTools "mcp__skywalker-mcp__*"
# Run a spectrum sweep via natural language
claude -p "Sweep the full IF band and tell me what you find" \
--mcp-config .mcp.json \
--allowedTools "mcp__skywalker-mcp__*"
```