Skip to content

birdcage API

The birdcage package provides a layered Python API for controlling Winegard satellite dishes. The architecture separates firmware communication (protocol), high-level control (antenna), network integration (rotctld), and predictive compensation (leapfrog).

cli.py Click CLI with init/serve/pos/move subcommands
|
v
antenna.py BirdcageAntenna: high-level control (consumers call this)
|
+---> leapfrog.py Pure function: apply_leapfrog(target, current)
|
v
protocol.py FirmwareProtocol ABC + HAL205Protocol / HAL000Protocol / CarryoutG2Protocol
Serial I/O owned here. Each firmware version is a subclass.
|
v
rotctld.py RotctldServer: Hamlib rotctld TCP protocol (p/P/S/_/q/R/L/D)
Bridges Gpredict to the antenna.

protocol.py — Firmware Protocol Abstraction

Section titled “protocol.py — Firmware Protocol Abstraction”
@dataclass
class Position:
"""Current dish orientation."""
azimuth: float
elevation: float
skew: float | None = None
@dataclass
class RssiReading:
"""Signal strength reading from the DVB subsystem."""
reads: int
average: int
current: int
MOTOR_AZIMUTH = 0
MOTOR_ELEVATION = 1

The abstract base class that all firmware implementations inherit from. Owns the serial connection and defines the contract for firmware interaction.

class FirmwareProtocol(ABC):
def connect(self, port: str, baudrate: int = 57600) -> None: ...
def disconnect(self) -> None: ...
def reset_to_root(self) -> None: ...
@abstractmethod
def initialize(self, callback: Callable[[str], None] | None = None) -> None: ...
@abstractmethod
def enter_motor_menu(self) -> None: ...
def get_position(self) -> Position: ...
def move_motor(self, motor_id: int, degrees: float) -> None: ...
@abstractmethod
def kill_search(self) -> None: ...
@property
def is_connected(self) -> bool: ...

Key methods:

MethodDescription
connect(port, baudrate)Open the serial connection (8N1, 1s timeout)
disconnect()Reset to root menu and close serial
initialize(callback)Wait for boot, kill satellite search. Callback receives status lines.
enter_motor_menu()Navigate into the motor control submenu
get_position()Query current AZ/EL/SK position (returns Position)
move_motor(motor_id, degrees)Command a single motor to an absolute position
kill_search()Cancel the firmware’s automatic TV satellite search
reset_to_root()Send q to return to root menu

HAL 2.05.003 firmware implementation.

  • Boot signals: "NoGPS" or "No LNB Voltage"
  • Motor submenu command: "motor"
  • Search kill sequence: ngsearch -> s -> q
  • Default baud: 57600

HAL 0.0.00 firmware implementation (older Trav’ler units).

  • Boot signal: "NoGPS"
  • Motor submenu command: "mot"
  • Search kill sequence: os -> kill Search -> q (OS task manager approach)
  • Default baud: 57600

Winegard Carryout G2 firmware implementation. Extends the base protocol with prompt-terminated reads, DVB signal measurement, and motor homing.

  • Default baud: 115200
  • Motor submenu command: "mot"
  • Search disable: NVS index 20 (permanent, no runtime kill needed)
  • Read strategy: Reads byte-by-byte until > (ASCII 62) prompt character
class CarryoutG2Protocol(FirmwareProtocol):
def connect(self, port: str, baudrate: int = 115200) -> None: ...
def get_position(self) -> Position: ... # Parses Angle[0]/Angle[1] format
def move_motor(self, motor_id: int, degrees: float) -> None: ...
def home_motor(self, motor_id: int) -> None: ...
# DVB / RSSI methods
def enter_dvb_menu(self) -> None: ...
def enable_lna(self) -> None: ...
def get_rssi(self, iterations: int = 10) -> RssiReading: ...
def quit_submenu(self) -> None: ...

G2-specific methods:

MethodDescription
home_motor(motor_id)Home a motor to its reference position via stall detection
enter_dvb_menu()Enter the DVB signal analysis submenu
enable_lna()Enable LNA in ODU mode (13V) for signal reception
get_rssi(iterations)Read averaged RSSI signal strength (returns RssiReading)
quit_submenu()Exit current submenu
FIRMWARE_REGISTRY: dict[str, type[FirmwareProtocol]] = {
"hal205": HAL205Protocol,
"hal000": HAL000Protocol,
"g2": CarryoutG2Protocol,
}
def get_protocol(name: str) -> FirmwareProtocol:
"""Instantiate a firmware protocol by short name.
Raises KeyError if name is not recognized.
"""
@dataclass
class AntennaConfig:
port: str = "/dev/ttyUSB0"
baudrate: int = 57600
min_elevation: float = 15.0
leapfrog_enabled: bool = True

The main interface that consumers (CLI, rotctld server, future MCP server) should use. Wraps the firmware protocol with position tracking, leap-frog compensation, and motor command alternation.

class BirdcageAntenna:
def __init__(
self,
protocol: FirmwareProtocol,
config: AntennaConfig | None = None,
) -> None: ...
@property
def config(self) -> AntennaConfig: ...
@property
def protocol(self) -> FirmwareProtocol: ...
@property
def is_connected(self) -> bool: ...
def connect(self) -> None: ...
def disconnect(self) -> None: ...
def initialize(self) -> None: ...
def get_position(self) -> Position: ...
def move_to(self, azimuth: float, elevation: float) -> None: ...
def stop(self) -> None: ...

Key behaviors:

MethodDescription
initialize()Connect (if needed), run boot sequence, enter motor menu
get_position()Query position and cache as _last_position for leapfrog
move_to(az, el)Move with leapfrog compensation + elevation floor enforcement. Motor commands alternate order (AZ-first on even moves, EL-first on odd) to prevent one axis from starving.
stop()Reset move counter

A pure function that compensates for mechanical motor lag during satellite tracking. When the delta between target and current position exceeds a threshold, the target is nudged further in the direction of travel.

def apply_leapfrog(
target_az: float,
target_el: float,
current_az: float,
current_el: float,
) -> tuple[float, float]:
"""Apply predictive overshoot to compensate for mechanical lag.
Returns adjusted (azimuth, elevation) with overshoot applied.
"""

Compensation table:

DeltaOvershoot applied
More than 2 degrees+/- 1.0 degree in direction of travel
More than 1 degree+/- 0.5 degree in direction of travel
1 degree or lessNo overshoot

Implements the subset of the Hamlib rotctld TCP protocol that Gpredict and other Hamlib clients use for AZ/EL rotor control. Extended with custom commands for sky-scan integration on the Carryout G2.

class RotctldServer:
def __init__(
self,
antenna: BirdcageAntenna,
host: str = "127.0.0.1",
port: int = 4533,
) -> None: ...
def serve_forever(self) -> None: ...
def shutdown(self) -> None: ...
CommandDescriptionResponse
pGet position<azimuth>\n<elevation>\n
P <az> <el>Set position (move dish)RPRT 0\n on success
SStop trackingCalls antenna.stop()
_Get model nameWinegard Trav'ler RS-485 Rotor\n
qQuit connectionCloses the TCP connection

These custom extensions are available when the underlying protocol is CarryoutG2Protocol. Non-G2 rotors return RPRT -6 (not available).

CommandDescriptionResponse
R [n]Read RSSI signal strength (n = iterations, default 10)<reads>\n<average>\n<current>\n
LEnable LNA for signal reception (one-time setup)RPRT 0\n on success
DDiscover supported capabilitiesCAPS:rssi,lna\n (G2) or CAPS:\n

Built with Click. All commands accept --port and --firmware options (also configurable via BIRDCAGE_PORT and BIRDCAGE_FIRMWARE environment variables).

Initialize the antenna: wait for boot, kill satellite search.

Terminal window
birdcage init --port /dev/ttyUSB0 --firmware hal205
birdcage init --port /dev/ttyUSB2 --firmware g2

Run a rotctld-compatible TCP server for Gpredict.

Terminal window
birdcage serve --port /dev/ttyUSB0 --firmware hal205
birdcage serve --port /dev/ttyUSB2 --firmware g2 --host 0.0.0.0 --listen-port 4533
birdcage serve --port /dev/ttyUSB2 --firmware g2 --skip-init
OptionDefaultDescription
--host127.0.0.1Address to listen on
--listen-port4533TCP port for rotctld protocol
--skip-initfalseSkip boot wait and search kill (dish already initialized)

Query and print the current dish position.

Terminal window
birdcage pos --port /dev/ttyUSB2 --firmware g2
# Output:
# AZ: 180.0
# EL: 45.0

Move the dish to a specific AZ/EL position.

Terminal window
birdcage move --port /dev/ttyUSB2 --firmware g2 --az 180.0 --el 45.0
birdcage move --port /dev/ttyUSB0 --firmware hal205 --az 90.0 --el 30.0 --no-leapfrog
OptionDescription
--azTarget azimuth (degrees, required)
--elTarget elevation (degrees, required)
--no-leapfrogDisable leap-frog compensation for this move
OptionEnv VarDefaultDescription
-v / --verbosefalseEnable debug logging
--portBIRDCAGE_PORT/dev/ttyUSB0Serial port
--firmwareBIRDCAGE_FIRMWAREhal205Firmware version (hal205, hal000, g2)