Initial commit: Birdcage documentation site
Starlight/Astro docs covering hardware reverse engineering, satellite tracking guides, firmware command reference, and engineering journal entries from the Carryout G2 exploration. 32 pages across getting-started, guides, reference, understanding, and journal sections.
This commit is contained in:
commit
088c1a5ace
44 changed files with 468217 additions and 0 deletions
245
src/content/docs/understanding/architecture.mdx
Normal file
245
src/content/docs/understanding/architecture.mdx
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
---
|
||||
title: Software Architecture
|
||||
description: How the birdcage and console-probe packages are structured, and why each layer exists.
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The project contains two Python packages. `birdcage` controls the dish for satellite tracking. `console-probe` is a separate tool for exploring and mapping unknown firmware consoles. They share no code at runtime but were developed together -- console-probe is how we learned enough about the firmware to write the protocol implementations in birdcage.
|
||||
|
||||
## birdcage
|
||||
|
||||
The control stack is five layers deep, each with a single responsibility:
|
||||
|
||||
```
|
||||
cli.py Click CLI: init / serve / pos / move
|
||||
|
|
||||
rotctld.py Hamlib rotctld TCP server (p/P/S/_/q + R/L/D extensions)
|
||||
|
|
||||
antenna.py BirdcageAntenna: high-level control, motor alternation, elevation floor
|
||||
|
|
||||
leapfrog.py Pure function: predictive overshoot compensation
|
||||
|
|
||||
protocol.py FirmwareProtocol ABC + HAL205 / HAL000 / CarryoutG2 subclasses
|
||||
(owns the serial port)
|
||||
```
|
||||
|
||||
Data flows top-down: Gpredict sends a position command to the rotctld server, which calls the antenna, which applies leapfrog correction and hands the adjusted target to the protocol, which sends the serial bytes to the dish.
|
||||
|
||||
### protocol.py -- firmware abstraction
|
||||
|
||||
This is where the serial port lives. The `FirmwareProtocol` abstract base class defines the contract that every firmware variant must implement:
|
||||
|
||||
```python
|
||||
class FirmwareProtocol(ABC):
|
||||
def connect(self, port: str, baudrate: int = 57600) -> None: ...
|
||||
def disconnect(self) -> None: ...
|
||||
def get_position(self) -> Position: ...
|
||||
def move_motor(self, motor_id: int, degrees: float) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self, callback: Callable[[str], None] | None = None) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def enter_motor_menu(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def kill_search(self) -> None: ...
|
||||
```
|
||||
|
||||
Three concrete subclasses implement this interface:
|
||||
|
||||
**`HAL205Protocol`** -- for HAL 2.05.003 firmware. Boot signals are `NoGPS` or `No LNB Voltage`. Enters the motor menu via the `motor` command. Kills the search by navigating `ngsearch` -> `s` -> `q`.
|
||||
|
||||
**`HAL000Protocol`** -- for HAL 0.0.00 firmware (older Trav'ler units). Same boot signal (`NoGPS`), but the motor command is `mot` and search kill goes through the OS task manager: `os` -> `kill Search` -> `q`.
|
||||
|
||||
**`CarryoutG2Protocol`** -- for the Carryout G2. RS-422 at 115200 baud instead of RS-485 at 57600. Search is disabled permanently via NVS, so `kill_search()` is a no-op. This subclass also adds methods not in the ABC: `home_motor()`, `enter_dvb_menu()`, `enable_lna()`, and `get_rssi()`.
|
||||
|
||||
The key architectural decision: **the protocol owns all serial I/O**. Nothing above this layer touches `pyserial` directly. The base class provides `_write()` and `_read()` for simple command/response. The G2 subclass overrides this with `_send()`, which reads byte-by-byte until the `>` prompt character (ASCII 62) -- more reliable than fixed-buffer reads because the firmware always emits `>` when ready.
|
||||
|
||||
```python
|
||||
def _send(self, cmd: str) -> str:
|
||||
"""Send a command and read until the '>' prompt character."""
|
||||
self._serial.write(f"{cmd}\r".encode("ascii"))
|
||||
|
||||
resp_data: bytearray = bytearray()
|
||||
while True:
|
||||
byte = self._serial.read(1)
|
||||
if len(byte) == 0:
|
||||
raise TimeoutError(f"No prompt after command: {cmd!r}")
|
||||
resp_data.append(byte[0])
|
||||
if byte[0] == self.PROMPT_CHAR:
|
||||
break
|
||||
|
||||
return resp_data.decode("utf-8", errors="ignore")
|
||||
```
|
||||
|
||||
A firmware registry maps short names to classes:
|
||||
|
||||
```python
|
||||
FIRMWARE_REGISTRY: dict[str, type[FirmwareProtocol]] = {
|
||||
"hal205": HAL205Protocol,
|
||||
"hal000": HAL000Protocol,
|
||||
"g2": CarryoutG2Protocol,
|
||||
}
|
||||
```
|
||||
|
||||
The CLI uses `get_protocol("g2")` to instantiate the right class. Adding a new firmware variant means writing a new subclass and adding it to this dictionary.
|
||||
|
||||
### leapfrog.py -- mechanical lag compensation
|
||||
|
||||
A single pure function with no side effects:
|
||||
|
||||
```python
|
||||
def apply_leapfrog(
|
||||
target_az: float, target_el: float,
|
||||
current_az: float, current_el: float,
|
||||
) -> tuple[float, float]:
|
||||
```
|
||||
|
||||
For each axis, if the delta between target and current position exceeds a threshold, the target is nudged further in the direction of travel:
|
||||
|
||||
| Delta | Overshoot |
|
||||
|-------|-----------|
|
||||
| More than 2 degrees | +/- 1.0 degree |
|
||||
| More than 1 degree | +/- 0.5 degree |
|
||||
| 1 degree or less | No adjustment |
|
||||
|
||||
This compensates for the time it takes stepper motors to physically reach a position -- by the time the dish arrives, the satellite has moved further along its track.
|
||||
|
||||
<Aside type="note">
|
||||
The original upstream code had a copy-paste bug where the elevation section modified `target_az` instead of `target_el`. This meant elevation never got leap-frog correction, and azimuth got double correction. See [Known Bugs](/understanding/known-bugs/) for the full analysis.
|
||||
</Aside>
|
||||
|
||||
### antenna.py -- the consumer-facing API
|
||||
|
||||
`BirdcageAntenna` is what everything above the protocol should call. It wraps three concerns:
|
||||
|
||||
1. **Lifecycle management** -- connect, initialize (boot wait + search kill + motor menu entry), disconnect
|
||||
2. **Leap-frog integration** -- applies `apply_leapfrog()` to every move if `config.leapfrog_enabled` is true
|
||||
3. **Motor command alternation** -- even-numbered moves send AZ first then EL; odd moves reverse the order. This prevents one axis from starving the other on the shared serial bus.
|
||||
|
||||
```python
|
||||
class BirdcageAntenna:
|
||||
def __init__(self, protocol: FirmwareProtocol, config: AntennaConfig | None = None):
|
||||
...
|
||||
|
||||
def initialize(self) -> None: ...
|
||||
def get_position(self) -> Position: ...
|
||||
def move_to(self, azimuth: float, elevation: float) -> None: ...
|
||||
def stop(self) -> None: ...
|
||||
```
|
||||
|
||||
`AntennaConfig` holds serial port, baud rate, minimum elevation, and the leapfrog toggle. The G2 defaults to 115200 baud and 18-degree minimum elevation; the Trav'ler defaults to 57600 and 15 degrees.
|
||||
|
||||
### rotctld.py -- Gpredict bridge
|
||||
|
||||
A plain TCP socket server implementing the subset of the Hamlib rotctld protocol that Gpredict uses:
|
||||
|
||||
| Command | Handler | Response |
|
||||
|---------|---------|----------|
|
||||
| `p` | `_handle_get_position()` | `<az>\n<el>\n` |
|
||||
| `P <az> <el>` | `_handle_set_position()` | `RPRT 0\n` or `RPRT -1\n` |
|
||||
| `S` | `_handle_stop()` | (closes connection) |
|
||||
| `_` | `_handle_model_name()` | `Winegard Trav'ler RS-485 Rotor\n` |
|
||||
| `q` | (break loop) | (closes connection) |
|
||||
|
||||
For CarryoutG2 specifically, three extension commands are available:
|
||||
|
||||
| Command | Handler | Function |
|
||||
|---------|---------|----------|
|
||||
| `R [n]` | `_handle_read_rssi()` | Read RSSI averaged over n samples |
|
||||
| `L` | `_handle_enable_lna()` | Enable LNA for signal reception |
|
||||
| `D` | `_handle_capabilities()` | Report supported extensions |
|
||||
|
||||
The RSSI handler is noteworthy -- it has to switch firmware submenus mid-operation. It exits the motor menu, enters the DVB menu, reads RSSI, exits DVB, and re-enters the motor menu. Non-G2 rotors return `RPRT -6` (not available) for these commands.
|
||||
|
||||
### cli.py -- the entry point
|
||||
|
||||
A Click CLI with four subcommands:
|
||||
|
||||
- **`init`** -- connect, wait for boot, kill search, enter motor menu
|
||||
- **`serve`** -- run the rotctld TCP server (optionally skipping init)
|
||||
- **`pos`** -- query and print current AZ/EL
|
||||
- **`move`** -- send a single move command to a specific AZ/EL
|
||||
|
||||
All subcommands accept `--port` and `--firmware` options, with environment variable fallbacks (`BIRDCAGE_PORT`, `BIRDCAGE_FIRMWARE`).
|
||||
|
||||
## console-probe
|
||||
|
||||
The probe tool has a parallel but simpler architecture:
|
||||
|
||||
```
|
||||
cli.py argparse CLI: --discover-only, --deep, --submenu, --json
|
||||
|
|
||||
report.py JSON report generation (format_version 2)
|
||||
|
|
||||
discovery.py Auto-discovery, help parsing, submenu probing, candidate generation
|
||||
|
|
||||
serial_io.py Prompt-aware serial I/O
|
||||
|
|
||||
profile.py DeviceProfile + HelpEntry dataclasses
|
||||
```
|
||||
|
||||
### profile.py -- device model
|
||||
|
||||
Everything known about the attached console is stored in a `DeviceProfile`:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class DeviceProfile:
|
||||
port: str = "/dev/ttyUSB0"
|
||||
baud: int = 115200
|
||||
root_prompt: str = "" # e.g. "TRK>"
|
||||
prompts: list[str] = ... # all known prompts
|
||||
error_string: str = "" # e.g. "Invalid command."
|
||||
known_commands: set[str] = ... # from help output
|
||||
submenus: list[str] = ... # detected submenu names
|
||||
exit_cmd: str = "q"
|
||||
line_ending: str = "\r"
|
||||
submenu_help: dict[str, list[HelpEntry]] = ...
|
||||
```
|
||||
|
||||
Commands parsed from help output are captured as `HelpEntry` objects with name, description, and parameter syntax.
|
||||
|
||||
### serial_io.py -- prompt-terminated reads
|
||||
|
||||
The key insight in console-probe's serial I/O is the `_is_prompt_terminated()` function. Instead of reading a fixed number of bytes or waiting for a timeout, it checks whether the response ends with a recognized prompt string.
|
||||
|
||||
This required solving a subtle bug: help text like `help [<command>]` contains the `>` character inside parameter syntax (`<command>`). The function distinguishes between actual prompts and parameter syntax by checking for `[` brackets on the last line:
|
||||
|
||||
```python
|
||||
def _is_prompt_terminated(text: str, profile: DeviceProfile) -> bool:
|
||||
last_line = stripped.split("\n")[-1]
|
||||
|
||||
if profile.prompts:
|
||||
# Check known prompts (fast path)
|
||||
for p in profile.prompts:
|
||||
if last_stripped.endswith(p):
|
||||
return True
|
||||
# Accept PROMPT_RE match only if no brackets on that line
|
||||
if "[" not in last_line:
|
||||
m = PROMPT_RE.search(last_line)
|
||||
if m:
|
||||
return True
|
||||
return False
|
||||
|
||||
# No known prompts yet -- fallback to bare > check
|
||||
return stripped.endswith(">")
|
||||
```
|
||||
|
||||
### discovery.py -- the exploration engine
|
||||
|
||||
This module handles several distinct tasks:
|
||||
|
||||
**Help parsing** -- `parse_help_output()` extracts command names and submenu hints from firmware help text. It handles multiple formats: `command - description`, angle-bracket syntax (`Enter <a3981>`), double-space separated columns, and bare command names. A set of known parameter placeholders (`command`, `value`, `index`, etc.) prevents false positives.
|
||||
|
||||
**Submenu probing** -- `discover_submenu_help()` queries help in the current submenu and tries multiple help commands (`?` and `man`) for firmware with paginated output.
|
||||
|
||||
**Error detection** -- `detect_error_string()` sends a garbage command and captures the firmware's error message, which is then used to distinguish real command responses from error responses during probing.
|
||||
|
||||
**Command probing** -- `probe_commands()` iterates through candidate command strings, sends each one, and checks whether the response differs from the error string. It recovers from accidental submenu exits and shell terminations.
|
||||
|
||||
**Candidate generation** -- `generate_candidates()` builds a list of potential commands from single characters, two-letter combinations, common embedded debug commands (memory access, flash, boot, GPIO, SPI, I2C, etc.), and optional external wordlists.
|
||||
204
src/content/docs/understanding/hardware-platform.mdx
Normal file
204
src/content/docs/understanding/hardware-platform.mdx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
---
|
||||
title: Hardware Platform
|
||||
description: Deep dive into the Carryout G2's internal hardware — MCU, motor drivers, DVB tuner, and SPI bus topology.
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The Carryout G2 is the most thoroughly documented variant in this project. Its firmware is verbose at boot and exposes low-level hardware state through GPIO, ADC, and SPI submenu commands. This page covers what we've identified about the three major silicon components and how they interconnect.
|
||||
|
||||
## System overview
|
||||
|
||||
The G2 runs on three chips connected by two SPI buses:
|
||||
|
||||
```
|
||||
SPI1 (4 MHz)
|
||||
K60 MCU ────────────────────────── 2x A3981 (Stepper Drivers)
|
||||
(Cortex-M4) AZ motor (40000 steps/rev)
|
||||
| EL motor (24960 steps/rev)
|
||||
|
|
||||
| SPI2 (6.857 MHz)
|
||||
└──────────────────────────── BCM4515 (DVB-S2 Tuner)
|
||||
RSSI, SNR, lock detect
|
||||
DiSEqC 2.x controller
|
||||
```
|
||||
|
||||
The MCU runs the firmware shell, motor control loop, and DVB signal processing. The motor drivers are pure slave peripherals -- they receive step/direction commands over SPI and handle microstepping and current regulation. The DVB tuner is also a slave but much more complex, running its own firmware for RF signal processing.
|
||||
|
||||
## NXP MK60DN512VLQ10 (K60 MCU)
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| Core | ARM Cortex-M4 |
|
||||
| Clock | 96 MHz |
|
||||
| Flash | 512 KB |
|
||||
| RAM | 128 KB SRAM |
|
||||
| Package | 144-LQFP |
|
||||
| FlexNVM | EEPROM emulation (EE> submenu) |
|
||||
| GPIO | 5 ports (A-E), ~92 pins bonded |
|
||||
| SPI | 3 controllers (SPI0-SPI2) |
|
||||
| UART | 6 controllers (UART0-UART5) |
|
||||
|
||||
The K60 is an NXP Kinetis part, part of the K60 sub-family designed for industrial and motor control applications. Its Cortex-M4 core includes a single-precision FPU, which the firmware uses for angle calculations (position readouts are floating-point).
|
||||
|
||||
<Aside type="note" title="Firmware version">
|
||||
The boot log reports firmware version **02.02.48**, copyright 2013, Winegard Company. Bootloader version 1.01. The firmware is stored in the K60's internal 512 KB flash and runs directly from there (no external code memory).
|
||||
</Aside>
|
||||
|
||||
### UART4 -- serial console
|
||||
|
||||
The firmware console runs on UART4 at 115200 baud, 8N1:
|
||||
|
||||
| K60 Pin | GPIO | Function |
|
||||
|---------|------|----------|
|
||||
| PTE24 | E24 | UART4_TX (to computer RX pair) |
|
||||
| PTE25 | E25 | UART4_RX (from computer TX pair) |
|
||||
| PTE26 | E26 | UART4_CTS (hardware flow control, idle high) |
|
||||
|
||||
CTS is connected but the firmware doesn't appear to require hardware flow control -- the console works fine without it.
|
||||
|
||||
### GPIO mapping
|
||||
|
||||
Live GPIO probing (`gpio regs`, `gpio dir`, `gpio r`) across ports A-E revealed 92 pins. Notable patterns:
|
||||
|
||||
- **Port E** is densely used: SPI1 (E0-E5), UART4 (E24-E27), plus unidentified pins
|
||||
- **Port D** carries SPI2 (D11-D15) and an output on D10 (likely BCM4515 reset/enable)
|
||||
- **Port B** has a cluster at B0-B3 (possibly SPI0 or I2C) and B11 (status LED or peripheral enable)
|
||||
- **Port C** has a contiguous block at C10-C13 (bus interface) and C18 (LNB voltage control)
|
||||
- **Absent pins:** A20-A23 and B12-B15 are not bonded on this package variant
|
||||
|
||||
## Allegro A3981 (Stepper Motor Drivers)
|
||||
|
||||
Two A3981 ICs, one per motor axis, controlled over SPI1.
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| Interface | SPI (mode 0x03) at 4 MHz |
|
||||
| Microstepping | Up to 1/16 step, AUTO mode |
|
||||
| Current control | AUTO mode (adapts to load) |
|
||||
| Fault detection | DIAG pin per driver (active-low open-drain) |
|
||||
| Step resolution | Full, Half, Quarter, Eighth, Sixteenth |
|
||||
|
||||
### Motor specifications
|
||||
|
||||
| Parameter | AZ (Motor 0) | EL (Motor 1) |
|
||||
|-----------|--------------|--------------|
|
||||
| Steps per revolution | 40000 | 24960 |
|
||||
| Max velocity | 65.00 deg/s | 45.00 deg/s |
|
||||
| Max acceleration | 400.00 deg/s^2 | (from NVS) |
|
||||
| Step velocity (raw) | 7222 ustep/s | 3120 ustep/s |
|
||||
| Step acceleration (raw) | 44 ustep/s/ms | 28 ustep/s/ms |
|
||||
| Gear ratio | 1.602564 | (from boot log) |
|
||||
|
||||
The AZ motor is the "master" (more steps, faster) and drives the entire dish rotation. The EL motor is the "slave" with fewer steps per revolution.
|
||||
|
||||
### SPI1 pin assignments
|
||||
|
||||
| K60 Pin | GPIO | SPI Function | Notes |
|
||||
|---------|------|-------------|-------|
|
||||
| PTE1 | E1 | SPI1_SOUT | MOSI -- MCU to A3981 |
|
||||
| PTE2 | E2 | SPI1_SCK | SPI clock |
|
||||
| PTE3 | E3 | SPI1_SIN | MISO -- A3981 to MCU |
|
||||
| PTE4 | E4 | SPI1_PCS0 | Chip select: AZ motor driver |
|
||||
| PTE0 | E0 | SPI1_PCS1 | Chip select: EL motor driver |
|
||||
| PTE5 | E5 | SPI1_PCS2 | Possibly A3981 RESET or enable |
|
||||
|
||||
<Aside type="note" title="AUTO mode">
|
||||
The `a3981 cm` command reports both drivers in "AUTO" current mode, and `a3981 sm` reports "AUTO" step mode. In AUTO mode, the A3981 selects the microstepping level based on the commanded step rate -- coarser steps at high speed for efficiency, finer steps at low speed for precision.
|
||||
</Aside>
|
||||
|
||||
### Motor control hierarchy
|
||||
|
||||
Three layers of motor control exist in the firmware:
|
||||
|
||||
1. **STEP submenu** -- raw stepper API in microstep units. Commands like `p [motor] [steps]` and `v [motor] [ustep/sec]` operate directly on the step counters.
|
||||
2. **MOT submenu** -- angle-based API. The `a <id> <deg>` command converts degrees to steps using the steps-per-revolution values and manages PID control.
|
||||
3. **BirdcageAntenna** (our code) -- adds leap-frog compensation and motor alternation on top of MOT commands.
|
||||
|
||||
### Homing and calibration
|
||||
|
||||
On boot (when tracker is enabled), the firmware runs a homing sequence:
|
||||
|
||||
```
|
||||
EL home: stall detect, 2 second timeout
|
||||
AZ home: stall detect, 8 second timeout
|
||||
"Antenna Facing Front"
|
||||
```
|
||||
|
||||
The dish uses motor stalling (not limit switches) to find mechanical boundaries. The A3981's stall detection watches for back-EMF signatures that indicate the motor has hit a hard stop.
|
||||
|
||||
After homing, the firmware reports cable wrap limits: `wrap_min:-42333 wrap_max:2333` (centidegrees). Total AZ range is approximately 446.66 degrees.
|
||||
|
||||
The `h <id>` command triggers explicit homing for a single motor outside the boot sequence.
|
||||
|
||||
<Aside type="caution" title="Unhomed axes">
|
||||
When NVS 20 is TRUE (tracker disabled), homing is skipped entirely on boot. Motor positions read as INT_MAX (2147483647) until manually homed with `h 0` and `h 1`. The ADC `scan` command will target INT_MAX on an unhomed axis and deadlock the shell.
|
||||
</Aside>
|
||||
|
||||
## Broadcom BCM4515 (DVB-S2 Tuner)
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| Chip ID | 0x4515 |
|
||||
| Silicon revision | B0 |
|
||||
| Firmware version | v113.37 |
|
||||
| Strap config | 0x25018 |
|
||||
| Interface | SPI (mode 0x03) at 6.857 MHz |
|
||||
| Standard | DVB-S2 (Digital Video Broadcasting - Satellite, 2nd gen) |
|
||||
| Search range | 18000-24000 ksps, rolloff 0.35 |
|
||||
|
||||
The BCM4515 is a highly integrated DVB-S2 demodulator. It handles the entire receive chain from IF input to transport stream output: RF/IF AGC, carrier recovery, timing recovery, LDPC/BCH decoding, and baseband processing.
|
||||
|
||||
### SPI2 pin assignments
|
||||
|
||||
| K60 Pin | GPIO | SPI Function | Notes |
|
||||
|---------|------|-------------|-------|
|
||||
| PTD12 | D12 | SPI2_SCK | SPI clock |
|
||||
| PTD13 | D13 | SPI2_SOUT | MOSI -- MCU to BCM4515 |
|
||||
| PTD14 | D14 | SPI2_SIN | MISO -- BCM4515 to MCU |
|
||||
| PTD11 | D11 | SPI2_PCS0 | Chip select |
|
||||
| PTD15 | D15 | SPI2_PCS1 | Secondary chip select (unused) |
|
||||
| PTD10 | D10 | GPIO (OUT) | Likely BCM4515 reset or power enable |
|
||||
|
||||
### Signal measurement capabilities
|
||||
|
||||
The DVB submenu exposes several signal measurement modes useful for ham radio:
|
||||
|
||||
**RSSI** -- `rssi <n>` returns the average and current raw ADC values over n samples. Noise floor is approximately 500. Used for RF power measurement at each pointing position.
|
||||
|
||||
**AGC** -- `agc` streams real-time RF/IF AGC levels plus SNR and NID (Network ID). This is a continuous output interrupted by `q`.
|
||||
|
||||
**SNR** -- Signal-to-noise ratio in dB.
|
||||
|
||||
**Lock status** -- `ls` returns total reads, no-signal count, glitch count, and NID table. `qls` gives a quick check.
|
||||
|
||||
**LNB control** -- `lnbdc odu` sets the LNB to 13V (V-polarization). Boot default is 18V (H-polarization). The `lnbv` command streams the actual voltage being applied.
|
||||
|
||||
### DiSEqC 2.x interface
|
||||
|
||||
The BCM4515 includes a DiSEqC controller for LNB switch control. DiSEqC uses 22 kHz tone bursts superimposed on the coax LNB bias line. The DVB submenu provides:
|
||||
|
||||
- **`di2conf`** -- LNB config register read
|
||||
- **`di2id`** -- Hardware ID query
|
||||
- **`di2stat`** -- LNB status flags
|
||||
- **`send <hex>`** -- Raw DiSEqC packet transmission (up to 6 bytes)
|
||||
- **`ovraddr`** -- Override target address (default 0x11)
|
||||
|
||||
Without an external DiSEqC switch connected, these commands return `RxReplyTimeout`.
|
||||
|
||||
### Radio telescope mode
|
||||
|
||||
The `azscanwxp` command in the MOT submenu performs an azimuth sweep while cycling through DVB transponders at each position. At each step, it records motor angle, RSSI, lock status, SNR, and scan delta. Combined with elevation stepping, this produces a 2D RF power map of the sky -- essentially using the dish as a radio telescope.
|
||||
|
||||
```
|
||||
azscanwxp [motor] [span_deg] [resolution_cdeg] [num_xponders]
|
||||
```
|
||||
|
||||
Output per position:
|
||||
```
|
||||
Motor:<id> Angle:<cdeg> RSSI:<adc> Lock:<0/1> SNR:<dB> Scan Delta:<step>
|
||||
```
|
||||
|
||||
This capability exists in the stock firmware -- it was designed for Winegard's factory alignment and satellite acquisition testing, but it works just as well for amateur radio sky mapping.
|
||||
192
src/content/docs/understanding/known-bugs.mdx
Normal file
192
src/content/docs/understanding/known-bugs.mdx
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
---
|
||||
title: Known Bugs
|
||||
description: Documented bugs in the upstream code and firmware, with analysis and fixes.
|
||||
sidebar:
|
||||
order: 4
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
This page documents bugs we've found and fixed in the upstream code, as well as firmware hazards that can't be fixed in software but need to be understood.
|
||||
|
||||
## Leap-frog elevation bug (upstream)
|
||||
|
||||
**Location:** `Trav-ler-Rotor-For-HAL-2.05/travler_rotor.py`, lines 98-105
|
||||
|
||||
**Severity:** Tracking accuracy degraded on the elevation axis
|
||||
|
||||
**Affected repos:**
|
||||
- `saveitforparts/Trav-ler-Rotor-For-HAL-2.05` -- `travler_rotor.py` lines 98-105
|
||||
- `saveitforparts/Travler-Pro-Rotor` -- same bug copy-pasted into `travler_pro_rotor.py`
|
||||
|
||||
### The bug
|
||||
|
||||
The leap-frog algorithm has two sections: azimuth compensation and elevation compensation. Both were written from the same template. In the elevation section, a copy-paste error left `target_az` as the variable being modified instead of changing it to `target_el`.
|
||||
|
||||
**Original code (lines 90-105):**
|
||||
|
||||
```python
|
||||
# Azimuth compensation (correct)
|
||||
if target_az - current_az > 2:
|
||||
target_az+=1
|
||||
elif target_az - current_az < -2:
|
||||
target_az-=1
|
||||
elif target_az - current_az > 1:
|
||||
target_az+=0.5
|
||||
elif target_az - current_az < -1:
|
||||
target_az-=0.5
|
||||
|
||||
# Elevation compensation (BUG: modifies target_az instead of target_el)
|
||||
if target_el - current_el > 2:
|
||||
target_az+=1 # <-- should be target_el
|
||||
elif target_el - current_el < -2:
|
||||
target_az-=1 # <-- should be target_el
|
||||
elif target_el - current_el > 1:
|
||||
target_az+=0.5 # <-- should be target_el
|
||||
elif target_el - current_el < -1:
|
||||
target_az-=0.5 # <-- should be target_el
|
||||
```
|
||||
|
||||
### Impact
|
||||
|
||||
Two things go wrong simultaneously:
|
||||
|
||||
1. **Elevation never gets leap-frog compensation.** The elevation delta is computed correctly (`target_el - current_el`), but the adjustment is applied to the wrong variable. During fast satellite passes with significant elevation change, the dish lags behind on the EL axis.
|
||||
|
||||
2. **Azimuth gets double compensation.** The azimuth correction from its own section is applied first, then the elevation section adds a second correction to the same variable. If both axes have large deltas (common during a pass), azimuth overshoots its target.
|
||||
|
||||
For a satellite pass where both AZ and EL are changing by more than 2 degrees per update:
|
||||
- AZ gets +2.0 degrees of correction (1.0 from its own section + 1.0 from the elevation section)
|
||||
- EL gets +0.0 degrees of correction
|
||||
|
||||
### The fix
|
||||
|
||||
In `birdcage/leapfrog.py`, the elevation section correctly modifies `target_el`:
|
||||
|
||||
```python
|
||||
def apply_leapfrog(
|
||||
target_az: float,
|
||||
target_el: float,
|
||||
current_az: float,
|
||||
current_el: float,
|
||||
) -> tuple[float, float]:
|
||||
# Azimuth compensation
|
||||
az_delta = target_az - current_az
|
||||
if abs(az_delta) > 2:
|
||||
target_az += 1.0 if az_delta > 0 else -1.0
|
||||
elif abs(az_delta) > 1:
|
||||
target_az += 0.5 if az_delta > 0 else -0.5
|
||||
|
||||
# Elevation compensation (fixed: modifies target_el, not target_az)
|
||||
el_delta = target_el - current_el
|
||||
if abs(el_delta) > 2:
|
||||
target_el += 1.0 if el_delta > 0 else -1.0
|
||||
elif abs(el_delta) > 1:
|
||||
target_el += 0.5 if el_delta > 0 else -0.5
|
||||
|
||||
return target_az, target_el
|
||||
```
|
||||
|
||||
The fix also restructures the conditionals to use `abs()` and ternary expressions, making the symmetry between the two axes explicit and harder to get wrong in future edits.
|
||||
|
||||
## Prompt termination bug (console-probe)
|
||||
|
||||
**Location:** `console_probe/serial_io.py`, `_is_prompt_terminated()`
|
||||
|
||||
**Severity:** False prompt detection causes truncated responses
|
||||
|
||||
### The bug
|
||||
|
||||
The original prompt detection logic checked whether the response ended with `>`. This worked for the firmware prompt (`TRK>`, `MOT>`, etc.) but also matched the `>` character inside parameter syntax in help text:
|
||||
|
||||
```
|
||||
help [<command>]
|
||||
```
|
||||
|
||||
The `>` in `<command>` would trigger prompt detection, cutting off the rest of the help output.
|
||||
|
||||
### The fix
|
||||
|
||||
The `_is_prompt_terminated()` function now distinguishes between prompts and parameter syntax using two strategies:
|
||||
|
||||
1. **Known prompt matching** -- when the profile has a list of known prompts (`TRK>`, `MOT>`, `DVB>`, etc.), it checks for exact suffix matches. This is the fast path and avoids false positives entirely.
|
||||
|
||||
2. **Bracket filtering** -- for pattern-based detection (when known prompts aren't populated yet), the function rejects any line containing `[` brackets before accepting a `>` match. Parameter syntax like `[<command>]` always appears inside brackets.
|
||||
|
||||
```python
|
||||
def _is_prompt_terminated(text: str, profile: DeviceProfile) -> bool:
|
||||
last_line = stripped.split("\n")[-1]
|
||||
|
||||
if profile.prompts:
|
||||
# Check known prompts first (fast path)
|
||||
for p in profile.prompts:
|
||||
if last_stripped.endswith(p):
|
||||
return True
|
||||
# Accept PROMPT_RE match only if no brackets on that line
|
||||
if "[" not in last_line:
|
||||
m = PROMPT_RE.search(last_line)
|
||||
if m:
|
||||
return True
|
||||
return False
|
||||
|
||||
# No known prompts yet -- fallback to bare > check
|
||||
return stripped.endswith(">")
|
||||
```
|
||||
|
||||
During initial discovery (before any prompts are known), the fallback to `stripped.endswith(">")` is intentionally permissive -- it may occasionally truncate, but it gets the first prompt detected so the more precise logic can take over.
|
||||
|
||||
## ADC scan deadlock (firmware hazard)
|
||||
|
||||
<Aside type="danger" title="This can brick your session">
|
||||
The ADC `scan` command without arguments on an uncalibrated azimuth axis will deadlock the firmware shell. **No serial input can recover it** -- not CR, Ctrl+C, ESC, `q`, or `reboot`. The only recovery is a hardware power cycle.
|
||||
</Aside>
|
||||
|
||||
### What happens
|
||||
|
||||
The ADC submenu's `scan` command performs an azimuth sweep with RSSI readings. When called without explicit position arguments, it reads the current AZ target from the motor controller.
|
||||
|
||||
If the AZ motor has not been homed (which happens when NVS 20 disables the tracker, skipping the boot homing sequence), the position register contains the sentinel value **2147483647** (INT_MAX, or 0x7FFFFFFF).
|
||||
|
||||
The firmware interprets this as a real target position and commands the motor to move there. The motor task blocks on this impossible move, and because the firmware shell is single-threaded -- UART input parsing only happens between command completions -- the shell becomes permanently unresponsive.
|
||||
|
||||
### Why serial input can't help
|
||||
|
||||
The K60's UART4 receive buffer fills up with the bytes you send (CR, `q`, etc.), but the main loop never reads them because it's stuck inside the motor move handler. There is no interrupt-based command abort mechanism in this firmware. The motor task runs to completion (or forever, in this case) before control returns to the shell parser.
|
||||
|
||||
### Mitigation
|
||||
|
||||
- Always home both axes before using ADC `scan`: run `mot` -> `h 0` (AZ) and `h 1` (EL) first
|
||||
- The `birdcage` software never calls ADC `scan` directly
|
||||
- The `console-probe` tool's timeout-based reads will eventually time out, but the firmware shell itself remains dead
|
||||
|
||||
### Other commands affected
|
||||
|
||||
Any command that internally reads motor position and initiates a move could theoretically hit this on an unhomed axis. The `azscanwxp` command in the MOT submenu is similarly dangerous without homing. However, simple position queries (`a` in MOT) safely return the INT_MAX value without attempting a move.
|
||||
|
||||
## Root menu `q` command (firmware design, not a bug)
|
||||
|
||||
The `q` command at the `TRK>` root prompt terminates the firmware shell task. This is by design -- it's a clean shutdown of the command interpreter. But the consequence is identical to the scan deadlock: the console becomes unresponsive and requires a power cycle.
|
||||
|
||||
Inside submenus, `q` safely exits to the parent menu. The hazard is only at the root level.
|
||||
|
||||
The `birdcage` code's `reset_to_root()` method sends `q` to exit submenus, which is safe. But if called when already at root, it would kill the shell. The CarryoutG2Protocol avoids this by using `_send("q")` which reads until the prompt -- if the shell dies, `_send` raises a `TimeoutError` instead of silently losing the connection.
|
||||
|
||||
## `command` false positive in help parsing
|
||||
|
||||
During automated probing, the word `command` appeared as a discovered command in the root menu. This is a false positive extracted from the help text:
|
||||
|
||||
```
|
||||
help [<command>]
|
||||
```
|
||||
|
||||
The help parser saw `<command>` as a valid angle-bracket command name. The fix in `console_probe/discovery.py` maintains a set of known parameter placeholders:
|
||||
|
||||
```python
|
||||
_PARAM_PLACEHOLDERS: set[str] = {
|
||||
"command", "commands", "parameter", "parameters",
|
||||
"value", "values", "index", "name", "arg", "args",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Any word matching this set is rejected during help parsing, preventing it from appearing as a discovered command.
|
||||
180
src/content/docs/understanding/reverse-engineering.mdx
Normal file
180
src/content/docs/understanding/reverse-engineering.mdx
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
---
|
||||
title: Reverse Engineering Methodology
|
||||
description: How we mapped the Carryout G2 firmware — from first serial connection to 100+ commands across 12 submenus.
|
||||
sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
This page documents the techniques used to reverse-engineer the Winegard Carryout G2 firmware console. The same approach applies to any embedded device with a serial debug console -- the tools and patterns are general-purpose.
|
||||
|
||||
## The seven phases
|
||||
|
||||
The firmware mapping happened in stages, each building on what the previous phase revealed.
|
||||
|
||||
### Phase 1: Serial connection and prompt discovery
|
||||
|
||||
The first step is finding the right electrical interface and baud rate. For the G2, Davidson's winegard-sky-scan project documented RS-422 at 115200 baud, so we didn't have to brute-force it. For an unknown device, you would:
|
||||
|
||||
1. Identify the connector type and pin count (RJ-12 6P6C in this case)
|
||||
2. Use a multimeter to find ground, then probe for differential pairs
|
||||
3. Try common baud rates: 9600, 19200, 38400, 57600, 115200
|
||||
4. Look for readable ASCII in the output at each rate
|
||||
|
||||
Once connected at the right baud rate, send a bare carriage return (`\r`). If the device has a command shell, it will echo a prompt. The G2 responds with `TRK>`.
|
||||
|
||||
<Aside type="tip" title="Polarity matters">
|
||||
RS-422 has separate TX and RX differential pairs. If you get garbled data at the correct baud rate, the RX pair polarity is inverted -- swap the `+` and `-` wires. If you get no response at all, the TX pair polarity is wrong (the dish can't decode your commands, so it silently ignores them).
|
||||
</Aside>
|
||||
|
||||
### Phase 2: Help command and initial inventory
|
||||
|
||||
Nearly every embedded console responds to `?` or `help`. The G2 supports both:
|
||||
|
||||
```
|
||||
TRK> ?
|
||||
```
|
||||
|
||||
This prints the root menu command list. Each line follows the pattern `Enter <command> - Description`, which gives us both the command name and a hint about whether it enters a submenu.
|
||||
|
||||
From this single command, we got the list of 12 top-level commands (submenus) plus a few root-level commands like `reboot`, `stow`, and `q`.
|
||||
|
||||
### Phase 3: Automated probing with console-probe
|
||||
|
||||
The `?` command only shows what the firmware documents. Many commands exist that aren't listed in help. The `console-probe` tool automates the process of finding them.
|
||||
|
||||
The discovery sequence:
|
||||
|
||||
1. **Detect the prompt** -- send a bare `\r`, read back the response, extract the prompt pattern
|
||||
2. **Detect the error string** -- send a garbage command (`__xyzzy_probe__`), capture the error message. This becomes the filter: any response that contains the error string is "not a valid command"
|
||||
3. **Parse help output** -- extract command names and submenu hints from the `?` response
|
||||
4. **Generate candidates** -- build a list of potential commands: single characters, two-letter combos, common embedded debug commands, and words from external wordlists
|
||||
5. **Probe each candidate** -- send it, check if the response differs from the error string. If it does, record it as a hit
|
||||
|
||||
```bash
|
||||
console-probe --port /dev/ttyUSB2 --baud 115200 --discover-only --json /tmp/discovery.json
|
||||
```
|
||||
|
||||
The probe tool handles recovery from several hazards during probing:
|
||||
|
||||
- **Accidental submenu entry** -- if a command enters a submenu (detected by a prompt change), the tool navigates back to the previous menu level
|
||||
- **Shell termination** -- if a command kills the shell (the `q` command at root), the tool waits for the firmware to restart
|
||||
- **Streaming commands** -- some commands produce continuous output; the timeout-based read strategy handles these by stopping after no new data arrives
|
||||
|
||||
### Phase 4: Interactive submenu exploration
|
||||
|
||||
Automated probing misses commands that require parameters. For example, `a 0 180.0` is a valid motor command, but probing just `a` gives a position readout (which is useful too), and probing `a 0` may return a partial error.
|
||||
|
||||
For each submenu, we entered it manually and typed `?`:
|
||||
|
||||
```
|
||||
TRK> mot
|
||||
MOT> ?
|
||||
```
|
||||
|
||||
This revealed the full set of 25 MOT commands, including parameter-requiring ones like `a <id> <deg>`, `g <az> <el>`, `pid [motor] [Kp] [Kv] [Ki]`, and `azscanwxp [motor] [span] [resolution] [num_xponders]`.
|
||||
|
||||
The DVB submenu has paginated help -- `?` shows the first page and `man` shows the second. The probe tool tries `man` automatically as an extra help command, but we discovered this through interactive use first.
|
||||
|
||||
Some submenus have commands that only the interactive `?` reveals:
|
||||
|
||||
| Submenu | Commands found by probe | Commands found by `?` only |
|
||||
|---------|------------------------|---------------------------|
|
||||
| MOT | 7 (a, e, l, life, p, r, v) | 18 more (g, h, pid, azscan, azscanwxp, ...) |
|
||||
| DVB | 5 (config, dis, lnbv, nid, snr) | 33 more (rssi, agc, table, send, ...) |
|
||||
| STEP | 4 (e, ma, mv, r) | 3 more (p, pid, v) |
|
||||
|
||||
The total went from ~40 probed hits to over 100 documented commands.
|
||||
|
||||
### Phase 5: NVS dump and configuration space
|
||||
|
||||
The NVS (Non-Volatile Storage) submenu provides `d` to dump all stored values:
|
||||
|
||||
```
|
||||
TRK> nvs
|
||||
NVS> d
|
||||
```
|
||||
|
||||
This returns every NVS index with its name, current value, saved value, and default value. The dump revealed 133+ configuration entries covering motor limits, PID gains, search behavior, sleep timers, elevation bounds, and satellite configurations.
|
||||
|
||||
Key discoveries from the NVS dump:
|
||||
|
||||
- **Index 20** (`Disable Tracker Proc?`) -- the switch that permanently disables TV satellite search
|
||||
- **Indices 80-88** -- motor velocity and acceleration limits, steps per revolution
|
||||
- **Indices 101-103** -- elevation min/max/home angles
|
||||
- **Indices 128-133** -- PID tuning parameters for the motor control loop
|
||||
|
||||
NVS values can be modified with `e <idx> <value>` and committed with `s`. This is how the tracker is disabled for amateur radio use.
|
||||
|
||||
<Aside type="tip" title="NVS is not EEPROM">
|
||||
The G2 has both an NVS submenu and an EEPROM (EE>) submenu. They are different storage systems. NVS is the firmware's primary configuration store with named parameters. The EEPROM is a lower-level K60 FlexNVM interface that mostly reads as uninitialized (0 or 0x10101). The firmware uses NVS for almost everything.
|
||||
</Aside>
|
||||
|
||||
### Phase 6: GPIO probing and hardware mapping
|
||||
|
||||
The GPIO submenu provides direct access to the K60's pin states:
|
||||
|
||||
```
|
||||
TRK> gpio
|
||||
GPIO> regs
|
||||
```
|
||||
|
||||
The `regs` command dumps all GPIO pin states across ports A through E -- 92 pins total. Cross-referencing these with the K60 datasheet's pin multiplexing table identifies which pins are configured for which peripheral (SPI, UART, GPIO, etc.).
|
||||
|
||||
The process:
|
||||
|
||||
1. Run `gpio regs` to get the state of every pin
|
||||
2. Run `gpio dir <pin>` on interesting pins to determine INPUT vs OUTPUT
|
||||
3. Match pin clusters against K60 peripheral assignments from the datasheet
|
||||
4. Correlate with boot log messages (e.g., "SPI1 init @ 4 MHz" tells us which SPI controller connects to the motor drivers)
|
||||
|
||||
This revealed the complete SPI1 (motor drivers) and SPI2 (DVB tuner) pin assignments, the UART4 console pins, and several unidentified outputs that are likely LNB control, status LEDs, and BCM4515 reset.
|
||||
|
||||
### Phase 7: Boot log analysis
|
||||
|
||||
Power-cycling the dish with a serial terminal attached captures the full boot sequence. The G2's boot log is detailed:
|
||||
|
||||
```
|
||||
Bootloader v1.01
|
||||
SPI1 init @ 4 MHz (mode 0x03)
|
||||
Motor init: System=12Inch, master=40000 steps, slave=24960 steps, ratio=1.602564
|
||||
SPI2 init @ 6.857 MHz (mode 0x03)
|
||||
EXTENDED_DVB_DEBUG ENABLED
|
||||
BCM4515 ID 0x4515 Rev B0, FW v113.37, strap 0x25018
|
||||
Auto-search config: blind scan, 18000-24000 ksps, rolloff 0.35
|
||||
Enabled LNB STB
|
||||
Ant ID - 12-IN G2
|
||||
```
|
||||
|
||||
Each line confirms a hardware detail: SPI bus speeds, motor step counts, DVB tuner identification, search parameters. The boot log is the single most information-dense source for understanding the hardware configuration.
|
||||
|
||||
## What console-probe automates vs. what needed manual work
|
||||
|
||||
| Task | Automated | Manual |
|
||||
|------|-----------|--------|
|
||||
| Prompt detection | Yes | -- |
|
||||
| Error string detection | Yes | -- |
|
||||
| Help text parsing | Yes | -- |
|
||||
| Submenu entry/exit | Yes | -- |
|
||||
| Brute-force command probing | Yes | -- |
|
||||
| Paginated help (`man`) | Partially (tries it) | Discovered manually |
|
||||
| Parameter-requiring commands | No | Full manual exploration |
|
||||
| NVS dump interpretation | No | Manual analysis |
|
||||
| GPIO/hardware correlation | No | Cross-reference with datasheet |
|
||||
| Boot log capture | No | Power-cycle with terminal open |
|
||||
| Safety hazard discovery | No | Learned the hard way (ADC `scan` deadlock) |
|
||||
|
||||
The automated tool gets you maybe 40% of the command surface. The remaining 60% requires connecting what the firmware tells you with datasheets, boot logs, and careful experimentation.
|
||||
|
||||
## Lessons learned
|
||||
|
||||
**Prompt-terminated reads are essential.** Fixed-timeout reads miss fast responses and waste time on slow ones. Reading until the `>` prompt character makes every interaction fast and reliable. But you have to handle the edge case where `>` appears inside parameter syntax (e.g., `help [<command>]`).
|
||||
|
||||
**Not every valid command is safe.** The ADC `scan` command without arguments on an unhomed axis targets position 2147483647 and deadlocks the shell forever. The root `q` command kills the firmware shell entirely. A probe tool needs to be cautious about commands that might brick the session.
|
||||
|
||||
**Submenu help reveals more than root help.** The root `?` command gives a one-line summary per submenu, but entering each submenu and typing `?` there reveals the full command set. Some submenus have multi-page help (DVB uses `man` for the second page).
|
||||
|
||||
**NVS is the Rosetta Stone.** The NVS dump gives you named configuration parameters with their defaults and current values. It tells you what the firmware considers configurable, which is a window into what the designers intended the device to do.
|
||||
|
||||
**Boot logs reveal hardware.** The firmware's own initialization messages are the most reliable documentation of what silicon is on the board -- chip IDs, bus speeds, memory sizes, step counts. This is faster and more accurate than trying to identify components visually on a PCB.
|
||||
Loading…
Add table
Add a link
Reference in a new issue