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

@ -64,9 +64,7 @@ async def test_start_creates_listening_server():
await pilot.pause()
# Post StartRequested with a known port.
panel.post_message(
TrackingPanel.StartRequested("127.0.0.1", 14533, 18.0)
)
panel.post_message(TrackingPanel.StartRequested("127.0.0.1", 14533, 18.0))
await _wait_for_listening(app)
status = app.query_one("#tracking-status", TrackingStatus)
@ -74,9 +72,7 @@ async def test_start_creates_listening_server():
assert control._rotctld_server is not None
# Verify the TCP port is open.
reader, writer = await asyncio.open_connection(
"127.0.0.1", 14533
)
reader, writer = await asyncio.open_connection("127.0.0.1", 14533)
writer.close()
await writer.wait_closed()
@ -102,9 +98,7 @@ async def test_stop_shuts_down_server():
await pilot.pause()
# Start.
panel.post_message(
TrackingPanel.StartRequested("127.0.0.1", 14534, 18.0)
)
panel.post_message(TrackingPanel.StartRequested("127.0.0.1", 14534, 18.0))
await _wait_for_listening(app)
assert control._rotctld_server is not None
@ -119,9 +113,7 @@ async def test_stop_shuts_down_server():
# Port should no longer be accepting.
with pytest.raises(OSError):
_r, _w = await asyncio.open_connection(
"127.0.0.1", 14534
)
_r, _w = await asyncio.open_connection("127.0.0.1", 14534)
@pytest.mark.asyncio
@ -147,16 +139,12 @@ async def test_status_display_updates():
assert status.moves == 0
# Start server.
panel.post_message(
TrackingPanel.StartRequested("127.0.0.1", 14535, 18.0)
)
panel.post_message(TrackingPanel.StartRequested("127.0.0.1", 14535, 18.0))
await _wait_for_listening(app)
assert status.state == "LISTENING"
# Connect a client -- should transition to CONNECTED.
reader, writer = await asyncio.open_connection(
"127.0.0.1", 14535
)
reader, writer = await asyncio.open_connection("127.0.0.1", 14535)
# Send a command so the server accept loop processes it.
writer.write(b"_\n")
await writer.drain()
@ -195,25 +183,17 @@ async def test_rotctld_get_position():
control.switch_mode("track")
await pilot.pause()
panel.post_message(
TrackingPanel.StartRequested("127.0.0.1", 14536, 18.0)
)
panel.post_message(TrackingPanel.StartRequested("127.0.0.1", 14536, 18.0))
await _wait_for_listening(app)
reader, writer = await asyncio.open_connection(
"127.0.0.1", 14536
)
reader, writer = await asyncio.open_connection("127.0.0.1", 14536)
# Query position.
writer.write(b"p\n")
await writer.drain()
az_line = await asyncio.wait_for(
reader.readline(), timeout=3.0
)
el_line = await asyncio.wait_for(
reader.readline(), timeout=3.0
)
az_line = await asyncio.wait_for(reader.readline(), timeout=3.0)
el_line = await asyncio.wait_for(reader.readline(), timeout=3.0)
az = float(az_line.decode().strip())
el = float(el_line.decode().strip())
@ -244,14 +224,10 @@ async def test_rotctld_set_position():
control.switch_mode("track")
await pilot.pause()
panel.post_message(
TrackingPanel.StartRequested("127.0.0.1", 14537, 18.0)
)
panel.post_message(TrackingPanel.StartRequested("127.0.0.1", 14537, 18.0))
await _wait_for_listening(app)
reader, writer = await asyncio.open_connection(
"127.0.0.1", 14537
)
reader, writer = await asyncio.open_connection("127.0.0.1", 14537)
# Move to a specific position.
writer.write(b"P 200.0 55.0\n")
@ -297,14 +273,10 @@ async def test_rotctld_quit_command():
control.switch_mode("track")
await pilot.pause()
panel.post_message(
TrackingPanel.StartRequested("127.0.0.1", 14538, 18.0)
)
panel.post_message(TrackingPanel.StartRequested("127.0.0.1", 14538, 18.0))
await _wait_for_listening(app)
reader, writer = await asyncio.open_connection(
"127.0.0.1", 14538
)
reader, writer = await asyncio.open_connection("127.0.0.1", 14538)
writer.write(b"q\n")
await writer.drain()