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
This commit is contained in:
parent
3ccec3be10
commit
bf72e784d3
72
README.md
Normal file
72
README.md
Normal file
@ -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 <id> <deg>` |
|
||||||
|
| Trav'ler (HAL 2.05) | RS-485 / RJ-25 | 57600 | `a <id> <deg>` |
|
||||||
|
| Trav'ler Pro | USB A-to-A | 57600 | `a <id> <deg>` |
|
||||||
|
| Carryout | RS-485 / RJ-25 | 57600 | `g <az> <el>` |
|
||||||
|
| Carryout G2 | RS-422 / RJ-12 | 115200 | `a <id> <deg>` |
|
||||||
|
|
||||||
|
## 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
|
||||||
58
mcp/README.md
Normal file
58
mcp/README.md
Normal file
@ -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
|
||||||
@ -6,6 +6,7 @@ build-backend = "hatchling.build"
|
|||||||
name = "mcbirdcage"
|
name = "mcbirdcage"
|
||||||
version = "2026.02.17"
|
version = "2026.02.17"
|
||||||
description = "MCP server for Winegard satellite dish control via serial"
|
description = "MCP server for Winegard satellite dish control via serial"
|
||||||
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
||||||
|
|||||||
40
mcp/tests/test_prompts.py
Normal file
40
mcp/tests/test_prompts.py
Normal file
@ -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
|
||||||
62
mcp/tests/test_resources.py
Normal file
62
mcp/tests/test_resources.py
Normal file
@ -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
|
||||||
@ -6,6 +6,7 @@ build-backend = "hatchling.build"
|
|||||||
name = "winegard-birdcage"
|
name = "winegard-birdcage"
|
||||||
version = "2026.02.17"
|
version = "2026.02.17"
|
||||||
description = "Winegard satellite dish control for amateur radio sky tracking"
|
description = "Winegard satellite dish control for amateur radio sky tracking"
|
||||||
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
||||||
|
|||||||
59
tui/README.md
Normal file
59
tui/README.md
Normal file
@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
|
||||||
@ -6,6 +6,7 @@ build-backend = "hatchling.build"
|
|||||||
name = "birdcage-tui"
|
name = "birdcage-tui"
|
||||||
version = "2026.02.17"
|
version = "2026.02.17"
|
||||||
description = "Textual TUI for Winegard satellite dish control and amateur radio sky tracking"
|
description = "Textual TUI for Winegard satellite dish control and amateur radio sky tracking"
|
||||||
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user