Fix G2 position/RSSI parsers, document motor and DVB test results

Position parser now matches the actual Angle[0]/Angle[1] format
instead of falling back to fragile raw-float extraction. RSSI parser
uses a proper named-group regex matching the real firmware output
format (Reads:<n> RSSI[avg: <v> cur: <v>]) — the old index-based
approach would fail on the actual 5-field response.

Motor test results: both axes move correctly, direction-dependent
overshoot of 0.01-0.06 degrees confirmed. DVB subsystem explored:
BCM4515 Rev B0, firmware v113.37, full command set documented
including DiSEqC 2.x, transponder scanning, and streaming AGC/SNR.
RSSI noise floor is ~500.
This commit is contained in:
Ryan Malloy 2026-02-12 09:34:42 -07:00
parent 71ffafdd3f
commit 6b94f079aa
3 changed files with 178 additions and 25 deletions

View File

@ -64,12 +64,12 @@ Five known Winegard dish variants documented by Gabe Emerson (KL1FI) / saveitfor
- **Carryout DIP switches:** All switches to off (up) may disable search mode, but behavior varies by unit. - **Carryout DIP switches:** All switches to off (up) may disable search mode, but behavior varies by unit.
- **Carryout G2 uses `a` not `g`:** Unlike the 2003 Carryout, the G2 uses standard `a <id> <deg>` motor addressing and the `mot` submenu — protocol-compatible with the Trav'ler family. - **Carryout G2 uses `a` not `g`:** Unlike the 2003 Carryout, the G2 uses standard `a <id> <deg>` motor addressing and the `mot` submenu — protocol-compatible with the Trav'ler family.
- **Carryout G2 is RS-422 full-duplex:** Separate TX/RX pairs at 115200 baud via RJ-12 6P6C, vs. RS-485 half-duplex at 57600 on the Trav'ler variants. Tested with DSD TECH SH-U11 USB-to-RS422 adapter (FTDI FT232R). **Polarity matters** — A/B (or +/-) labeling is not standardized; if you get garbled data at the correct baud rate, swap the +/- wires on the RX pair. TX pair polarity swap causes the dish to not receive commands (silent failure). - **Carryout G2 is RS-422 full-duplex:** Separate TX/RX pairs at 115200 baud via RJ-12 6P6C, vs. RS-485 half-duplex at 57600 on the Trav'ler variants. Tested with DSD TECH SH-U11 USB-to-RS422 adapter (FTDI FT232R). **Polarity matters** — A/B (or +/-) labeling is not standardized; if you get garbled data at the correct baud rate, swap the +/- wires on the RX pair. TX pair polarity swap causes the dish to not receive commands (silent failure).
- **Carryout G2 position format differs from Trav'ler:** Position query `a` in `MOT>` submenu returns `Angle[0] = 180.00` / `Angle[1] = 45.00` — not the `AZ = / EL =` format used by HAL 0.0.00 and HAL 2.05. The `CarryoutG2Protocol` parser needs to handle this format. - **Carryout G2 position format differs from Trav'ler:** Position query `a` in `MOT>` submenu returns `Angle[0] = 180.00` / `Angle[1] = 45.00` — not the `AZ = / EL =` format used by HAL 0.0.00 and HAL 2.05. Move confirmation returns `Angle = 46.00` (no array index). `CarryoutG2Protocol.get_position()` uses `Angle\[0\]`/`Angle\[1\]` regex. Motor overshoot is direction-dependent: +0.010.05° in travel direction, -0.020.06° on return (stepper backlash).
- **Carryout G2 firmware version 02.02.48** confirmed (Copyright 2013 - Winegard Company). Bootloader version 1.01. MCU: Kinetis (NXP ARM Cortex-M). DVB tuner: BCM4515 (Broadcom). - **Carryout G2 firmware version 02.02.48** confirmed (Copyright 2013 - Winegard Company). Bootloader version 1.01. MCU: Kinetis (NXP ARM Cortex-M). DVB tuner: BCM4515 (Broadcom).
- **Carryout G2 boot sequence:** Bootloader → SPI init → Motor init (System=12Inch, master=40000 steps, slave=24960 steps, ratio=1.602564) → DVB tuner init (BCM4515) → NVS load → EL home (stall detect, 2s timeout) → AZ home (stall detect, 8s timeout) → `Antenna Facing Front``TRK>` prompt (if tracker disabled) or search start. - **Carryout G2 boot sequence:** Bootloader → SPI init → Motor init (System=12Inch, master=40000 steps, slave=24960 steps, ratio=1.602564) → DVB tuner init (BCM4515) → NVS load → EL home (stall detect, 2s timeout) → AZ home (stall detect, 8s timeout) → `Antenna Facing Front``TRK>` prompt (if tracker disabled) or search start.
- **Carryout G2 cable wrap:** Confirmed from homing output: `wrap_min:-42333 wrap_max:2333` (centidegrees). Total range ~446.66°. - **Carryout G2 cable wrap:** Confirmed from homing output: `wrap_min:-42333 wrap_max:2333` (centidegrees). Total range ~446.66°.
- **Carryout G2 has `h <id>` homing:** Explicit motor home-to-reference command. Not documented on other variants. - **Carryout G2 has `h <id>` homing:** Explicit motor home-to-reference command. Not documented on other variants.
- **Carryout G2 has DVB/RSSI:** Signal strength measurement via `dvb` submenu (`lnbdc odu` to enable LNA, `rssi <n>` to sample). Used for sky scanning / RF imaging. Atmospheric baseline measured at boot: ~494 (18V) / ~499 (13V) wideband. - **Carryout G2 has DVB/RSSI:** BCM4515 tuner (ID 0x4515, Rev B0, firmware v113.37). DVB submenu provides `rssi <n>` (bounded, returns `Reads:<n> RSSI[avg: <v> cur: <v>]`), `agc` (streaming RF/IF AGC + SNR + NID), `snr`, `lnbdc odu` (enable LNA 13V), `lnbv` (streaming voltage monitor), `dis` (channel params), `config` (hardware ID), `table` (transponder scan), and DiSEqC 2.x commands (`di2*`, `send`). RSSI noise floor is ~500. `lnbdc odu` sets 13V (V-pol); boot default is 18V (H-pol). Streaming commands run until interrupted by `q` or another command.
## Hardware Specs (SK-1000) ## Hardware Specs (SK-1000)
@ -209,9 +209,23 @@ nvs — enter non-volatile storage submenu
e <idx> — read NVS value e <idx> — read NVS value
e <idx> <v> — write NVS value e <idx> <v> — write NVS value
s — save changes s — save changes
dvb — signal info / LNB signal strength submenu dvb — DVB tuner submenu (BCM4515)
lnbdc odu — enable LNA in ODU mode (powers LNB for reception) config — hardware/firmware version
rssi <n> — read RSSI signal strength averaged over n samples dis — display channel parameters (frequency, symbol rate, LNB polarity, etc.)
lnbdc odu — enable LNA in ODU mode (13V = V-pol; boot default 18V = H-pol)
lnbv — stream LNB voltage readings (continuous, interrupt with q)
rssi <n> — RSSI averaged over n samples (bounded, returns avg + cur)
snr — SNR level
agc — stream RF/IF AGC + SNR + NID (continuous, interrupt with q)
ls — lock status
qls — quick lock status
t <n> — select transponder
table — generate transponder table
e <n> <v> — edit channel parameter
freqs — tuner frequency list
di2id — DiSEqC read LNB hardware ID
di2stat — DiSEqC read LNB status flags
send <hex> — raw DiSEqC packet (max 6 bytes, space-delimited hex)
reboot — reboot firmware reboot — reboot firmware
stow — fold dish flat (caution: modified feeds may not survive) stow — fold dish flat (caution: modified feeds may not survive)
``` ```

View File

@ -153,6 +153,141 @@ From homing output: `wrap_min:-42333 wrap_max:2333`
- In centidegrees: -423.33° to +23.33° from home position - In centidegrees: -423.33° to +23.33° from home position
- Total range: 446.66° (~1.24 full rotations) - Total range: 446.66° (~1.24 full rotations)
## Motor Control
### Position Query
In the `MOT>` submenu, `a` returns position with 4-space indentation:
```
a
Angle[0] = 180.00 ← AZ (degrees)
Angle[1] = 45.00 ← EL (degrees)
MOT>
```
### Move Command
`a <id> <deg>` returns a confirmation (no array index) and the prompt immediately
while the motor moves in the background:
```
a 1 46
Angle = 46.00
MOT>
```
### Observed Motor Behavior
| Test | Command | Target | Actual | Overshoot |
|------|---------|--------|--------|-----------|
| EL move out | `a 1 46` | 46.00 | 46.05 | +0.05° |
| AZ move out | `a 0 181` | 181.00 | 181.01 | +0.01° |
| EL return | `a 1 45` | 45.00 | 44.94 | -0.06° |
| AZ return | `a 0 180` | 180.00 | 179.98 | -0.02° |
Direction-dependent overshoot: the motor consistently overshoots in the
direction of travel, undershooting on return. This is classic stepper
backlash + PID settling behavior and is what the leapfrog algorithm
compensates for.
## DVB Subsystem (BCM4515)
### Hardware
```
BCM Hardware= ID: 0x4515 VER: 0xB0
BCM Firmware= MAJOR VER: 0x71 (113) MINOR VER: 0x25 (37)
BCM Strap Config: 0x25018
```
### Channel Parameters (`dis`)
```
Power Mode: ON
Search Transponders: ON
Auto Search Mode: 1
Shuffle Mode: ON
Frequency List: Non-Stacked
Num Parameter Current Default
1 Frequency 1090640 (kHz) 974000 (kHz)
2 Symbol Rate 0 (PeakScanEnabled) 20000 (ksps)
3 Trans_Mod_CRate blind_scan blind_scan
4 Blind Scan Mode ___trb_dvb_dss_____ ___trb_dvb_dss_____
5 LNB Polarity ODU:13V ---
6 LNB Tone (ODU) off off
7 Roll-off 0.35 0.35
8 LPF Cutoff 0 (auto) 0 (MHz)
9 Carrier Offset 0 (kHz) 0 (kHz)
10 FreqSearchRange 5000 (kHz) 5000 (kHz)
11 DCII Mode dcii_qpsk_comb dcii_qpsk_comb
12 Spectral Inv scan scan
13 PScnSymRtRngMin 18000 (ksps) 18000 (ksps)
14 PScnSymRtRngMax 24000 (ksps) 24000 (ksps)
15 SignalDetectMode off off
```
### RSSI Response Format
```
rssi 5
iterations:5 interval(msec):20
Reads:5 RSSI[avg: 500 cur: 500]
DVB>
```
500 is the noise floor (no signal lock, dish pointed at arbitrary sky).
### LNB Voltage
`lnbdc odu` enables LNA at 13V. `lnbv` streams continuous voltage readings:
```
Reads:1 LNB Voltage (mV): 13239 ( ADC value: 119 )
Reads:2 LNB Voltage (mV): 13182 ( ADC value: 118 )
...
```
Stable at ~13.11V (ADC 117). Boot default is 18V; `lnbdc odu` switches to 13V.
13V = vertical polarization, 18V = horizontal polarization on a standard LNB.
### AGC (Streaming)
`agc` streams RF and IF automatic gain control plus SNR/NID:
```
Reads:1 RF_AGC[avg: 1327353088 cur: 1327353088] IF_AGC[avg: 2684354560 cur: 2684354560] SNR: 0.0 NID: FFFF/none
```
- RF_AGC values are raw BCM4515 32-bit register values
- IF_AGC constant at 0xA0000000 (fixed IF gain)
- SNR: 0.0 when no signal lock
- NID: FFFF/none = no DVB network ID detected
### DVB Command Reference
| Command | Type | Description |
|---------|------|-------------|
| `rssi <n>` | One-shot | Average signal strength over n samples |
| `snr` | Unknown | SNR level |
| `agc` | Streaming | RF/IF AGC + SNR + NID (runs until interrupted) |
| `lnbdc odu` | One-shot | Enable LNB in ODU mode (13V) |
| `lnbv` | Streaming | Continuous LNB voltage monitoring |
| `ls` | Unknown | Lock status |
| `qls` | Unknown | Quick lock status |
| `config` | One-shot | BCM hardware/firmware version |
| `dis` | One-shot | Display channel parameters |
| `freqs` | One-shot | Tuner frequency list |
| `diag` | Unknown | Diagnostic data |
| `t <n>` | One-shot | Select transponder |
| `table` | One-shot | Generate transponder table |
| `e <n> <v>` | One-shot | Edit channel parameter |
| `di2*` | One-shot | DiSEqC 2.x LNB commands |
| `send <hex>` | One-shot | Raw DiSEqC packet (max 6 bytes) |
Streaming commands (`agc`, `lnbv`) run until a new command or `q` interrupts them.
## Satellite Configuration ## Satellite Configuration
``` ```

View File

@ -312,29 +312,24 @@ class CarryoutG2Protocol(FirmwareProtocol):
def get_position(self) -> Position: def get_position(self) -> Position:
"""Query dish position. """Query dish position.
The G2 may return floats without AZ=/EL= labels, so we try the G2 firmware 02.02.48 returns position as::
labeled format first and fall back to raw float extraction.
Angle[0] = 180.00
Angle[1] = 45.00
MOT>
Where Angle[0] is azimuth and Angle[1] is elevation.
""" """
response = self._send("a") response = self._send("a")
# Try labeled format (AZ = / EL =) for compatibility # G2 format: Angle[0] = <az>, Angle[1] = <el>
az_match = re.search(r"AZ\s*=?\s*(\d+\.\d+)", response) az_match = re.search(r"Angle\[0\]\s*=\s*(-?\d+\.\d+)", response)
el_match = re.search(r"EL\s*=?\s*(\d+\.\d+)", response) el_match = re.search(r"Angle\[1\]\s*=\s*(-?\d+\.\d+)", response)
if az_match and el_match: if az_match and el_match:
sk_match = re.search(r"SK\s*=?\s*(\d+\.\d+)", response)
return Position( return Position(
azimuth=float(az_match.group(1)), azimuth=float(az_match.group(1)),
elevation=float(el_match.group(1)), elevation=float(el_match.group(1)),
skew=float(sk_match.group(1)) if sk_match else None,
)
# Fall back to raw float extraction (G2-style: just two numbers)
floats = re.findall(r"\d+\.\d+", response)
if len(floats) >= 2:
return Position(
azimuth=float(floats[0]),
elevation=float(floats[1]),
) )
raise ValueError(f"Could not parse position from: {response!r}") raise ValueError(f"Could not parse position from: {response!r}")
@ -367,6 +362,12 @@ class CarryoutG2Protocol(FirmwareProtocol):
def get_rssi(self, iterations: int = 10) -> RssiReading: def get_rssi(self, iterations: int = 10) -> RssiReading:
"""Read averaged RSSI signal strength (DVB submenu). """Read averaged RSSI signal strength (DVB submenu).
Firmware response format::
iterations:5 interval(msec):20
Reads:5 RSSI[avg: 500 cur: 500]
DVB>
Args: Args:
iterations: Number of samples to average. iterations: Number of samples to average.
@ -377,13 +378,16 @@ class CarryoutG2Protocol(FirmwareProtocol):
ValueError: If the RSSI response can't be parsed. ValueError: If the RSSI response can't be parsed.
""" """
response = self._send(f"rssi {iterations}") response = self._send(f"rssi {iterations}")
results = re.findall(r"\d+", response)
if len(results) >= 6: match = re.search(
r"Reads:(\d+)\s+RSSI\[avg:\s*(\d+)\s+cur:\s*(\d+)\]",
response,
)
if match:
return RssiReading( return RssiReading(
reads=int(results[3]), reads=int(match.group(1)),
average=int(results[4]), average=int(match.group(2)),
current=int(results[5]), current=int(match.group(3)),
) )
raise ValueError(f"Could not parse RSSI from: {response!r}") raise ValueError(f"Could not parse RSSI from: {response!r}")