Add Craft mode — direct satellite tracking via space.warehack.ing API

New F2 Control sub-mode that searches the Craft orbital catalog (22k+
objects), displays pass predictions, and drives the dish in real-time
using server-computed AZ/EL positions from /api/sky/up.

Tracking loop polls at ~1Hz, filters for the tracked target by
target_type + target_id (str, not int — handles satellites, planets,
stars, comets), and issues motor commands through the existing serial
bridge. Verified end-to-end with Carryout G2 hardware tracking NOAA 17.

New files:
- craft_client.py — stdlib HTTP client (urllib only, no deps)
- widgets/craft_panel.py — search table, pass info, tracking status
- tests/test_craft_mode.py — 5 unit tests with mocked API
- tests/test_craft_integration.py — 3 hardware integration tests
This commit is contained in:
Ryan Malloy 2026-02-16 02:07:42 -07:00
parent a249c98208
commit 6c1e9da773
9 changed files with 1379 additions and 85 deletions

View file

@ -54,6 +54,7 @@ class BirdcageApp(App):
serial_port: str = "/dev/ttyUSB0"
firmware_name: str = "g2"
skip_init: bool = False
craft_url: str = "https://space.warehack.ing"
device: object = None
shutdown_event: threading.Event = threading.Event()
@ -110,6 +111,7 @@ class BirdcageApp(App):
self.run_worker(self._initialize_device, thread=True)
self._distribute_device()
self._setup_craft_client()
self._update_status_strip_connection()
self._install_console()
self._start_position_poll()
@ -129,6 +131,18 @@ class BirdcageApp(App):
if hasattr(screen, "set_device"):
screen.set_device(self.device)
def _setup_craft_client(self) -> None:
"""Create a Craft API client and hand it to the control screen."""
from birdcage_tui.craft_client import CraftClient
client = CraftClient(base_url=self.craft_url)
try:
control = self.query_one("#control")
if hasattr(control, "set_craft_client"):
control.set_craft_client(client)
except Exception:
log.debug("Could not set craft client on control screen")
def _update_status_strip_connection(self) -> None:
"""Set the status strip's connection info from current device."""
strip = self.query_one("#status-strip", StatusStrip)
@ -310,6 +324,11 @@ def main() -> None:
parser.add_argument(
"--skip-init", action="store_true", help="Skip firmware initialization"
)
parser.add_argument(
"--craft-url",
default="https://space.warehack.ing",
help="Craft API base URL",
)
args = parser.parse_args()
app = BirdcageApp()
@ -317,6 +336,7 @@ def main() -> None:
app.serial_port = args.port
app.firmware_name = args.firmware
app.skip_init = args.skip_init
app.craft_url = args.craft_url
try:
app.run()
except KeyboardInterrupt: