From 6b94f079aa3e7eadb0c47998c32d42a254d956dc Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Thu, 12 Feb 2026 09:34:42 -0700 Subject: [PATCH] Fix G2 position/RSSI parsers, document motor and DVB test results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: RSSI[avg: cur: ]) — 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. --- CLAUDE.md | 24 ++++-- docs/g2-nvs-dump.md | 135 ++++++++++++++++++++++++++++++++++ src/travler_rotor/protocol.py | 44 ++++++----- 3 files changed, 178 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f75a260..b7c1ba4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 G2 uses `a` not `g`:** Unlike the 2003 Carryout, the G2 uses standard `a ` 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 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.01–0.05° in travel direction, -0.02–0.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 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 has `h ` 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 ` 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 ` (bounded, returns `Reads: RSSI[avg: cur: ]`), `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) @@ -209,9 +209,23 @@ nvs — enter non-volatile storage submenu e — read NVS value e — write NVS value s — save changes -dvb — signal info / LNB signal strength submenu - lnbdc odu — enable LNA in ODU mode (powers LNB for reception) - rssi — read RSSI signal strength averaged over n samples +dvb — DVB tuner submenu (BCM4515) + config — hardware/firmware version + 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 — 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 — select transponder + table — generate transponder table + e — edit channel parameter + freqs — tuner frequency list + di2id — DiSEqC read LNB hardware ID + di2stat — DiSEqC read LNB status flags + send — raw DiSEqC packet (max 6 bytes, space-delimited hex) reboot — reboot firmware stow — fold dish flat (caution: modified feeds may not survive) ``` diff --git a/docs/g2-nvs-dump.md b/docs/g2-nvs-dump.md index 0858f09..e868812 100644 --- a/docs/g2-nvs-dump.md +++ b/docs/g2-nvs-dump.md @@ -153,6 +153,141 @@ From homing output: `wrap_min:-42333 wrap_max:2333` - In centidegrees: -423.33° to +23.33° from home position - 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 ` 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 ` | 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 ` | One-shot | Select transponder | +| `table` | One-shot | Generate transponder table | +| `e ` | One-shot | Edit channel parameter | +| `di2*` | One-shot | DiSEqC 2.x LNB commands | +| `send ` | One-shot | Raw DiSEqC packet (max 6 bytes) | + +Streaming commands (`agc`, `lnbv`) run until a new command or `q` interrupts them. + ## Satellite Configuration ``` diff --git a/src/travler_rotor/protocol.py b/src/travler_rotor/protocol.py index ad78669..dc7bbc7 100644 --- a/src/travler_rotor/protocol.py +++ b/src/travler_rotor/protocol.py @@ -312,29 +312,24 @@ class CarryoutG2Protocol(FirmwareProtocol): def get_position(self) -> Position: """Query dish position. - The G2 may return floats without AZ=/EL= labels, so we try the - labeled format first and fall back to raw float extraction. + G2 firmware 02.02.48 returns position as:: + + Angle[0] = 180.00 + Angle[1] = 45.00 + MOT> + + Where Angle[0] is azimuth and Angle[1] is elevation. """ response = self._send("a") - # Try labeled format (AZ = / EL =) for compatibility - az_match = re.search(r"AZ\s*=?\s*(\d+\.\d+)", response) - el_match = re.search(r"EL\s*=?\s*(\d+\.\d+)", response) + # G2 format: Angle[0] = , Angle[1] = + az_match = re.search(r"Angle\[0\]\s*=\s*(-?\d+\.\d+)", response) + el_match = re.search(r"Angle\[1\]\s*=\s*(-?\d+\.\d+)", response) if az_match and el_match: - sk_match = re.search(r"SK\s*=?\s*(\d+\.\d+)", response) return Position( azimuth=float(az_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}") @@ -367,6 +362,12 @@ class CarryoutG2Protocol(FirmwareProtocol): def get_rssi(self, iterations: int = 10) -> RssiReading: """Read averaged RSSI signal strength (DVB submenu). + Firmware response format:: + + iterations:5 interval(msec):20 + Reads:5 RSSI[avg: 500 cur: 500] + DVB> + Args: iterations: Number of samples to average. @@ -377,13 +378,16 @@ class CarryoutG2Protocol(FirmwareProtocol): ValueError: If the RSSI response can't be parsed. """ 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( - reads=int(results[3]), - average=int(results[4]), - current=int(results[5]), + reads=int(match.group(1)), + average=int(match.group(2)), + current=int(match.group(3)), ) raise ValueError(f"Could not parse RSSI from: {response!r}")