Promote bridge, demo, craft_client to core birdcage package
Move bridge.py, demo.py, craft_client.py from tui/src/birdcage_tui/ to src/birdcage/ so both TUI and MCP server can share the device layer without a circular dependency on textual.
This commit is contained in:
parent
3013eeee4c
commit
16ca4892b3
@ -1,6 +1,15 @@
|
||||
"""birdcage: Winegard satellite dish control for amateur radio sky tracking."""
|
||||
|
||||
from birdcage.antenna import AntennaConfig, BirdcageAntenna
|
||||
from birdcage.bridge import SerialBridge
|
||||
from birdcage.craft_client import (
|
||||
CraftClient,
|
||||
CraftTrackingState,
|
||||
PassPrediction,
|
||||
SearchResult,
|
||||
TargetPosition,
|
||||
)
|
||||
from birdcage.demo import DemoCraftClient, DemoDevice
|
||||
from birdcage.leapfrog import apply_leapfrog
|
||||
from birdcage.protocol import (
|
||||
CarryoutG2Protocol,
|
||||
@ -16,11 +25,19 @@ __all__ = [
|
||||
"AntennaConfig",
|
||||
"BirdcageAntenna",
|
||||
"CarryoutG2Protocol",
|
||||
"CraftClient",
|
||||
"CraftTrackingState",
|
||||
"DemoCraftClient",
|
||||
"DemoDevice",
|
||||
"FirmwareProtocol",
|
||||
"HAL000Protocol",
|
||||
"HAL205Protocol",
|
||||
"PassPrediction",
|
||||
"Position",
|
||||
"RssiReading",
|
||||
"RotctldServer",
|
||||
"SearchResult",
|
||||
"SerialBridge",
|
||||
"TargetPosition",
|
||||
"apply_leapfrog",
|
||||
]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Thread-safe bridge between Birdcage TUI and CarryoutG2Protocol.
|
||||
"""Thread-safe bridge between consumers and CarryoutG2Protocol.
|
||||
|
||||
Wraps all serial I/O in a threading.Lock so the TUI's worker threads
|
||||
Wraps all serial I/O in a threading.Lock so concurrent callers
|
||||
don't stomp on each other. Tracks the current firmware submenu to
|
||||
minimize unnecessary q-then-reenter transitions.
|
||||
"""
|
||||
@ -53,7 +53,7 @@ _MENU_COMMANDS: dict[Menu, str] = {
|
||||
|
||||
|
||||
class SerialBridge:
|
||||
"""Thread-safe wrapper around CarryoutG2Protocol for TUI consumption.
|
||||
"""Thread-safe wrapper around CarryoutG2Protocol.
|
||||
|
||||
All public methods acquire a lock before touching the serial port.
|
||||
The bridge tracks the current firmware submenu so it can skip
|
||||
@ -68,7 +68,7 @@ class SerialBridge:
|
||||
self._connected = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Menu prompt → string mapping for status display
|
||||
# Menu prompt -> string mapping for status display
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
_MENU_PROMPTS: dict[Menu, str] = {
|
||||
@ -297,7 +297,7 @@ class SerialBridge:
|
||||
"el_accel": 0.0,
|
||||
}
|
||||
|
||||
# mv → "Max Vel [0] = 65.0 Max Vel [1] = 45.0" or similar
|
||||
# mv -> "Max Vel [0] = 65.0 Max Vel [1] = 45.0" or similar
|
||||
vel_matches = re.findall(r"Max Vel\s*\[(\d)\]\s*=\s*(-?\d+\.?\d*)", mv_resp)
|
||||
for motor_id, val in vel_matches:
|
||||
if motor_id == "0":
|
||||
@ -305,7 +305,7 @@ class SerialBridge:
|
||||
elif motor_id == "1":
|
||||
result["el_max_vel"] = float(val)
|
||||
|
||||
# ma → "Accel[0] = 400.0 Accel[1] = 400.0"
|
||||
# ma -> "Accel[0] = 400.0 Accel[1] = 400.0"
|
||||
acc_matches = re.findall(r"Accel\[(\d)\]\s*=\s*(-?\d+\.?\d*)", ma_resp)
|
||||
for motor_id, val in acc_matches:
|
||||
if motor_id == "0":
|
||||
@ -316,7 +316,7 @@ class SerialBridge:
|
||||
return result
|
||||
|
||||
def set_max_velocity(self, motor_id: int, deg_per_sec: float) -> None:
|
||||
"""Set max velocity for a motor axis (°/s). Firmware: MOT> mv [motor] [vel]."""
|
||||
"""Set max velocity for a motor axis (deg/s)."""
|
||||
with self._lock:
|
||||
self._ensure_menu(Menu.MOT)
|
||||
self._send(f"mv {motor_id} {deg_per_sec:.1f}")
|
||||
@ -324,7 +324,7 @@ class SerialBridge:
|
||||
def set_max_acceleration(self, motor_id: int, accel: float) -> None:
|
||||
"""Set max acceleration for a motor axis.
|
||||
|
||||
Firmware: MOT> ma [motor] [accel] (°/s²).
|
||||
Firmware: MOT> ma [motor] [accel] (deg/s^2).
|
||||
"""
|
||||
with self._lock:
|
||||
self._ensure_menu(Menu.MOT)
|
||||
@ -460,7 +460,7 @@ class SerialBridge:
|
||||
Args:
|
||||
start_az: Starting azimuth in degrees.
|
||||
span: Total sweep width in degrees.
|
||||
step_cdeg: Step size in centidegrees (100 = 1.00°).
|
||||
step_cdeg: Step size in centidegrees (100 = 1.00 deg).
|
||||
num_xponders: Number of transponders to cycle per position.
|
||||
timeout: Serial read timeout for the long-running command.
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Provides satellite search, pass predictions, and real-time sky positions
|
||||
from the Craft orbital mechanics API. All methods are blocking — designed
|
||||
to be called from @work(thread=True) workers in the TUI.
|
||||
to be called from worker threads.
|
||||
|
||||
Uses urllib.request + json only (no requests/httpx dependency).
|
||||
"""
|
||||
@ -1,4 +1,4 @@
|
||||
"""Synthetic demo device for the Birdcage TUI.
|
||||
"""Synthetic demo device for Birdcage.
|
||||
|
||||
Drop-in replacement for SerialBridge that simulates a Winegard Carryout G2
|
||||
dish with motor movement, RSSI signal modeling, and canned firmware responses.
|
||||
@ -16,7 +16,7 @@ import random
|
||||
import time
|
||||
from enum import Enum, auto
|
||||
|
||||
from birdcage_tui.craft_client import PassPrediction, SearchResult, TargetPosition
|
||||
from birdcage.craft_client import PassPrediction, SearchResult, TargetPosition
|
||||
|
||||
|
||||
class _DemoMenu(Enum):
|
||||
@ -812,14 +812,14 @@ def _leo_position(target_id: str, t: float) -> tuple[float, float]:
|
||||
"""
|
||||
offset, period, max_el = _LEO_ARCS.get(target_id, (0.0, 10.0, 40.0))
|
||||
period_sec = period * 60.0
|
||||
phase = ((t + offset * 60.0) % period_sec) / period_sec # 0.0 → 1.0
|
||||
phase = ((t + offset * 60.0) % period_sec) / period_sec # 0.0 -> 1.0
|
||||
|
||||
# Visible window: phase 0.0-0.5 = above horizon, 0.5-1.0 = below
|
||||
if phase > 0.5:
|
||||
return 0.0, -10.0 # Below horizon
|
||||
|
||||
# Map 0→0.5 to AZ 90→270, EL 0→max→0 (sine arc)
|
||||
arc_phase = phase / 0.5 # 0.0 → 1.0 through the visible pass
|
||||
# Map 0->0.5 to AZ 90->270, EL 0->max->0 (sine arc)
|
||||
arc_phase = phase / 0.5 # 0.0 -> 1.0 through the visible pass
|
||||
az = 90.0 + 180.0 * arc_phase
|
||||
el = max_el * math.sin(math.pi * arc_phase)
|
||||
return az, el
|
||||
@ -102,15 +102,14 @@ class BirdcageApp(App):
|
||||
def _setup_device(self) -> None:
|
||||
"""Create device (demo or real) and hand it to each screen."""
|
||||
if self.demo_mode:
|
||||
from birdcage_tui.demo import DemoDevice
|
||||
from birdcage.demo import DemoDevice
|
||||
|
||||
self.device = DemoDevice()
|
||||
self.device.connect()
|
||||
else:
|
||||
from birdcage.bridge import SerialBridge
|
||||
from birdcage.protocol import get_protocol
|
||||
|
||||
from birdcage_tui.bridge import SerialBridge
|
||||
|
||||
protocol = get_protocol(self.firmware_name)
|
||||
self.device = SerialBridge(protocol)
|
||||
self.device.connect(self.serial_port)
|
||||
@ -142,11 +141,11 @@ class BirdcageApp(App):
|
||||
def _setup_craft_client(self) -> None:
|
||||
"""Create a Craft API client and hand it to the control screen."""
|
||||
if self.demo_mode:
|
||||
from birdcage_tui.demo import DemoCraftClient
|
||||
from birdcage.demo import DemoCraftClient
|
||||
|
||||
client = DemoCraftClient()
|
||||
else:
|
||||
from birdcage_tui.craft_client import CraftClient
|
||||
from birdcage.craft_client import CraftClient
|
||||
|
||||
client = CraftClient(base_url=self.craft_url)
|
||||
try:
|
||||
|
||||
@ -9,6 +9,7 @@ import contextlib
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from birdcage.craft_client import CraftClient, CraftTrackingState
|
||||
from textual import work
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
@ -16,7 +17,6 @@ from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.widgets import Button, ContentSwitcher, Input, Static
|
||||
from textual.worker import Worker
|
||||
|
||||
from birdcage_tui.craft_client import CraftClient, CraftTrackingState
|
||||
from birdcage_tui.rotctld_server import TrackingState, TuiRotctldServer
|
||||
from birdcage_tui.widgets.compass_rose import CompassRose
|
||||
from birdcage_tui.widgets.craft_panel import CraftPanel
|
||||
|
||||
@ -13,7 +13,7 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from birdcage_tui.app import BirdcageApp
|
||||
from birdcage_tui.craft_client import PassPrediction, SearchResult, TargetPosition
|
||||
from birdcage.craft_client import PassPrediction, SearchResult, TargetPosition
|
||||
from birdcage_tui.screens.control import ControlScreen
|
||||
from birdcage_tui.widgets.craft_panel import CraftPanel, CraftTrackingStatus
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user