From bf72e784d30c7078ecde1b2227cfe7ea32e437d2 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 17 Feb 2026 19:15:23 -0700 Subject: [PATCH] Add PyPI README files and MCP resource/prompt tests - README.md for all three packages (core, TUI, MCP) - pyproject.toml readme field for PyPI rendering - 8 new tests for MCP resources (5) and prompts (3) - Total MCP test coverage: 57 tests, 37 tools + 5 resources + 3 prompts --- README.md | 72 +++++++++++++++++++++++++++++++++++++ mcp/README.md | 58 ++++++++++++++++++++++++++++++ mcp/pyproject.toml | 1 + mcp/tests/test_prompts.py | 40 +++++++++++++++++++++ mcp/tests/test_resources.py | 62 ++++++++++++++++++++++++++++++++ pyproject.toml | 1 + tui/README.md | 59 ++++++++++++++++++++++++++++++ tui/pyproject.toml | 1 + 8 files changed, 294 insertions(+) create mode 100644 README.md create mode 100644 mcp/README.md create mode 100644 mcp/tests/test_prompts.py create mode 100644 mcp/tests/test_resources.py create mode 100644 tui/README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f38be1 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# winegard-birdcage + +Serial control library for Winegard motorized satellite dishes, repurposed for amateur radio satellite tracking. + +Turns surplus RV/marine satellite TV antennas into steerable ground station dishes via RS-485 or RS-422. + +## Install + +```bash +pip install winegard-birdcage +``` + +## CLI Tools + +Two entry points are included: + +**birdcage** -- antenna control and rotctld server: + +```bash +birdcage init --port /dev/ttyUSB0 --firmware hal205 +birdcage pos +birdcage move --az 180.0 --el 45.0 +birdcage serve --host 127.0.0.1 --port 4533 # rotctld for Gpredict +``` + +**console-probe** -- automated firmware exploration: + +```bash +console-probe --port /dev/ttyUSB0 --baud 115200 --discover-only --json report.json +console-probe --port /dev/ttyUSB0 --baud 115200 --deep --wordlist wordlist.txt +``` + +## Supported Hardware + +| Variant | Connection | Baud | Motor Command | +|---------|-----------|------|---------------| +| Trav'ler (HAL 0.0.00) | RS-485 / RJ-25 | 57600 | `a ` | +| Trav'ler (HAL 2.05) | RS-485 / RJ-25 | 57600 | `a ` | +| Trav'ler Pro | USB A-to-A | 57600 | `a ` | +| Carryout | RS-485 / RJ-25 | 57600 | `g ` | +| Carryout G2 | RS-422 / RJ-12 | 115200 | `a ` | + +## Architecture + +``` +protocol.py -- FirmwareProtocol ABC + per-variant subclasses (HAL205, HAL000, G2) +leapfrog.py -- Predictive overshoot compensation for mechanical motor lag +antenna.py -- BirdcageAntenna: high-level control wrapping protocol + leapfrog +rotctld.py -- Hamlib rotctld TCP server (p/P/S/_/q) for Gpredict integration +cli.py -- Click CLI: init / serve / pos / move +``` + +## Related Packages + +| Package | Description | +|---------|-------------| +| [birdcage-tui](https://pypi.org/project/birdcage-tui/) | Six-screen terminal UI for dish control | +| [mcbirdcage](https://pypi.org/project/mcbirdcage/) | MCP server for AI-assisted dish operations | + +## Documentation + +Full hardware details, wiring guides, firmware command reference, and NVS tables: +**[birdcage.warehack.ing](https://birdcage.warehack.ing)** + +## Credits + +- **Gabe Emerson (KL1FI / [saveitforparts](https://github.com/saveitforparts))** -- original Trav'ler, Trav'ler Pro, and Carryout rotor scripts +- **Chris Davidson ([cdavidson0522](https://github.com/cdavidson0522))** -- Carryout G2 sky scan and rotator control + +## License + +MIT diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..a467ed8 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,58 @@ +# mcbirdcage + +[MCP](https://modelcontextprotocol.io/) server for controlling Winegard satellite dishes through conversational tools. Built on [FastMCP](https://gofastmcp.com/), exposes 36 tools for dish positioning, signal analysis, firmware inspection, and satellite pass planning. + +## Install + +```bash +# Add to Claude Code +claude mcp add mcbirdcage -- uvx mcbirdcage + +# Or run standalone +uvx mcbirdcage +``` + +## Demo Mode + +No dish required. Set `BIRDCAGE_DEMO=1` to get simulated responses for all tools: + +```bash +BIRDCAGE_DEMO=1 uvx mcbirdcage +``` + +## Tools + +36 tools across six groups: + +| Group | Count | Examples | +|-------|-------|---------| +| Connection | 3 | `connect`, `disconnect`, `status` | +| Movement | 9 | `get_position`, `move_to`, `home_motor`, `stow` | +| Signal | 8 | `get_rssi`, `enable_lna`, `az_sweep`, `get_lock_status` | +| System | 11 | `nvs_dump`, `get_firmware_id`, `set_pid_gains`, `get_a3981_diag` | +| Satellite | 4 | `search_satellites`, `get_passes`, `get_visible_targets` | +| Console | 1 | `send_raw_command` (direct firmware access) | + +Plus 5 resources (hardware specs, NVS reference, firmware docs) and 3 prompts. + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `BIRDCAGE_DEMO` | `0` | Enable demo mode (no hardware) | +| `BIRDCAGE_PORT` | `/dev/ttyUSB0` | Serial port path | +| `BIRDCAGE_FIRMWARE` | `hal205` | Firmware variant (`hal000`, `hal205`, `g2`) | +| `BIRDCAGE_CRAFT_URL` | -- | Craft API URL for live satellite TLEs | + +## Documentation + +Full tool reference and hardware setup: **[birdcage.warehack.ing](https://birdcage.warehack.ing)** + +## Credits + +- **Gabe Emerson (KL1FI / [saveitforparts](https://github.com/saveitforparts))** -- original Winegard rotor scripts +- **Chris Davidson ([cdavidson0522](https://github.com/cdavidson0522))** -- Carryout G2 sky scan and rotator control + +## License + +MIT diff --git a/mcp/pyproject.toml b/mcp/pyproject.toml index 774b7c2..369c209 100644 --- a/mcp/pyproject.toml +++ b/mcp/pyproject.toml @@ -6,6 +6,7 @@ build-backend = "hatchling.build" name = "mcbirdcage" version = "2026.02.17" description = "MCP server for Winegard satellite dish control via serial" +readme = "README.md" license = "MIT" requires-python = ">=3.11" authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}] diff --git a/mcp/tests/test_prompts.py b/mcp/tests/test_prompts.py new file mode 100644 index 0000000..f6537e0 --- /dev/null +++ b/mcp/tests/test_prompts.py @@ -0,0 +1,40 @@ +"""Tests for MCP prompts (setup_wizard, satellite_tracking_guide, rf_sweep_guide).""" + +import pytest +from fastmcp import Client + + +@pytest.mark.anyio +async def test_setup_wizard_prompt(mcp_client: Client): + result = await mcp_client.get_prompt("setup_wizard") + + assert result.messages, "setup_wizard returned no messages" + text = result.messages[0].content.text + assert len(text) > 0 + assert "connect" in text.lower() + assert "home_motor" in text + assert "get_position" in text + + +@pytest.mark.anyio +async def test_satellite_tracking_guide_prompt(mcp_client: Client): + result = await mcp_client.get_prompt("satellite_tracking_guide") + + assert result.messages, "satellite_tracking_guide returned no messages" + text = result.messages[0].content.text + assert len(text) > 0 + assert "search_satellites" in text + assert "get_passes" in text + assert "move_to" in text + + +@pytest.mark.anyio +async def test_rf_sweep_guide_prompt(mcp_client: Client): + result = await mcp_client.get_prompt("rf_sweep_guide") + + assert result.messages, "rf_sweep_guide returned no messages" + text = result.messages[0].content.text + assert len(text) > 0 + assert "enable_lna" in text + assert "get_rssi" in text + assert "az_sweep" in text diff --git a/mcp/tests/test_resources.py b/mcp/tests/test_resources.py new file mode 100644 index 0000000..9dfd014 --- /dev/null +++ b/mcp/tests/test_resources.py @@ -0,0 +1,62 @@ +"""Tests for MCP resources (config, position, firmware, motor-dynamics, el-limits).""" + +import json + +import pytest +from fastmcp import Client + + +@pytest.mark.anyio +async def test_config_resource(mcp_client: Client): + contents = await mcp_client.read_resource("birdcage://config") + data = json.loads(contents[0].text) + + assert data["demo_mode"] is True + assert data["connected"] is True + assert data["firmware"] == "g2" + assert data["serial_port"] == "/dev/demo" + + +@pytest.mark.anyio +async def test_position_resource(mcp_client: Client): + contents = await mcp_client.read_resource("birdcage://position") + data = json.loads(contents[0].text) + + assert "azimuth" in data + assert "elevation" in data + assert isinstance(data["azimuth"], float) + assert isinstance(data["elevation"], float) + # DemoDevice starts at AZ=180, EL=45 (approximately, with settling noise). + assert 170.0 < data["azimuth"] < 190.0 + assert 35.0 < data["elevation"] < 55.0 + + +@pytest.mark.anyio +async def test_firmware_resource(mcp_client: Client): + contents = await mcp_client.read_resource("birdcage://firmware") + text = contents[0].text + + assert "02.02.48" in text + assert "TWELINCH" in text + assert "K60-144pin" in text + + +@pytest.mark.anyio +async def test_motor_dynamics_resource(mcp_client: Client): + contents = await mcp_client.read_resource("birdcage://motor-dynamics") + data = json.loads(contents[0].text) + + assert data["az_max_vel"] == 65.0 + assert data["el_max_vel"] == 45.0 + assert data["az_accel"] == 400.0 + assert data["el_accel"] == 400.0 + + +@pytest.mark.anyio +async def test_el_limits_resource(mcp_client: Client): + contents = await mcp_client.read_resource("birdcage://el-limits") + data = json.loads(contents[0].text) + + assert data["min"] == 18.0 + assert data["max"] == 65.0 + assert data["home"] == 65.0 diff --git a/pyproject.toml b/pyproject.toml index 6210b6b..38db9c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ build-backend = "hatchling.build" name = "winegard-birdcage" version = "2026.02.17" description = "Winegard satellite dish control for amateur radio sky tracking" +readme = "README.md" license = "MIT" requires-python = ">=3.11" authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}] diff --git a/tui/README.md b/tui/README.md new file mode 100644 index 0000000..bc8e91b --- /dev/null +++ b/tui/README.md @@ -0,0 +1,59 @@ +# birdcage-tui + +Terminal UI for controlling Winegard satellite dishes. Built on [Textual](https://textual.textualize.io/) with six screens covering everything from manual pointing to live satellite tracking. + +Try it without hardware: + +```bash +uvx birdcage-tui --demo +``` + +## Install + +```bash +pip install birdcage-tui + +# With camera capture support (JPEG annotation, FITS export) +pip install birdcage-tui[camera] +``` + +## Screens + +| Key | Screen | What it does | +|-----|--------|-------------| +| F1 | Dashboard | Live AZ/EL readout, compass rose, motor status, signal gauge | +| F2 | Control | Manual jog, satellite presets, Track mode (rotctld), Craft mode (live TLE) | +| F3 | Signal | RSSI bargraph, azimuth sweep plot, sky heatmap | +| F4 | System | NVS editor, firmware ID, motor dynamics, PID tuning, A3981 diagnostics | +| F5 | Console | Raw serial terminal to the dish firmware | +| F6 | Camera | Capture overlay with multi-trigger pipeline (requires `camera` extra) | + +The collage below shows all six screens. On PyPI this image may not render -- see the [TUI guide](https://birdcage.warehack.ing/guides/tui/) for full screenshots. + +![Birdcage TUI](https://birdcage.warehack.ing/screenshots/tui-collage.png) + +## Usage + +```bash +# Demo mode (no dish required) +birdcage-tui --demo + +# Connect to hardware +birdcage-tui --port /dev/ttyUSB0 --firmware hal205 + +# Carryout G2 at 115200 baud +birdcage-tui --port /dev/ttyUSB2 --firmware g2 --baud 115200 +``` + +## Documentation + +Detailed screen walkthroughs and configuration: **[birdcage.warehack.ing/guides/tui](https://birdcage.warehack.ing/guides/tui/)** + +## Credits + +- **Gabe Emerson (KL1FI / [saveitforparts](https://github.com/saveitforparts))** -- original Winegard rotor scripts +- **Chris Davidson ([cdavidson0522](https://github.com/cdavidson0522))** -- Carryout G2 sky scan and rotator control + +## License + +MIT diff --git a/tui/pyproject.toml b/tui/pyproject.toml index d74fea4..a6a1f9d 100644 --- a/tui/pyproject.toml +++ b/tui/pyproject.toml @@ -6,6 +6,7 @@ build-backend = "hatchling.build" name = "birdcage-tui" version = "2026.02.17" description = "Textual TUI for Winegard satellite dish control and amateur radio sky tracking" +readme = "README.md" license = "MIT" requires-python = ">=3.11" authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]