From 7ff91b08ead4fa55ffde72406e6d17d0436a0303 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Thu, 12 Feb 2026 21:05:33 -0700 Subject: [PATCH] Refactor probe tool to generic embedded console scanner, document full G2 command inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrote hidden_menu_probe.py from Winegard-hardcoded to auto-discovering: detects prompt, error string, and submenu structure from any firmware console. Extracted Winegard-specific candidate words to scripts/wordlists/winegard.txt. Deep probe of all 12 G2 submenus discovered commands across A3981 (driver diagnostics), ADC (RSSI monitoring + position sweep), DVB (extended help via man, transponder selection), EEPROM (read/write), GPIO (pin R/W), LATLON (calculator), MOT (azscan, sw), PEAK (EchoStar switch), and STEP (raw stepper control). NVS submenu generates false positives — treats any input as sequential index reads. Safety: added q/Q to default blocklist, bare-CR check before navigate_to_root to prevent accidental shell termination between submenus. --- CLAUDE.md | 212 ++++- docs/g2-nvs-dump.md | 1436 +++++++++++++++++++++++++++++++- scripts/hidden_menu_probe.py | 1043 +++++++++++++++++++++++ scripts/wordlists/winegard.txt | 60 ++ 4 files changed, 2702 insertions(+), 49 deletions(-) create mode 100644 scripts/hidden_menu_probe.py create mode 100644 scripts/wordlists/winegard.txt diff --git a/CLAUDE.md b/CLAUDE.md index b7c1ba4..2458d06 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,8 @@ Five known Winegard dish variants documented by Gabe Emerson (KL1FI) / saveitfor | **Prompt char** | `>` (likely) | `>` (likely) | undocumented | undocumented | `TRK>` / `MOT>` / `NVS>` (confirmed) | | **Position format** | `AZ = / EL =` | `AZ = / EL =` | `AZ = / EL =` | raw ints / 100 | `Angle[0] = / Angle[1] =` | | **DVB tuner** | unknown | unknown | unknown | unknown | BCM4515 (Broadcom) | +| **MCU** | unknown | unknown | unknown | unknown | NXP MK60DN512VLQ10 (Kinetis K60, Cortex-M4, 96MHz, 512KB flash, 128KB RAM) | +| **Motor driver** | unknown | unknown | unknown | unknown | 2× Allegro A3981 (SPI, 1/16 microstep, AUTO mode) | ### Key Variant Differences @@ -189,45 +191,185 @@ For short cable runs (under ~3m between ESP32 and dish), the built-in 120 ohm te ### Firmware Console Commands +Full command inventory from automated deep probe (firmware 02.02.48, 2026-02-12). +Probed with `scripts/hidden_menu_probe.py --deep --wordlist scripts/wordlists/winegard.txt`. + +#### Root Menu (TRK>) + ``` -? — list available commands -motor / mot — enter motor submenu (firmware-dependent) -a — show position (in motor submenu) -a — move motor to absolute position -h — home motor to reference position (G2, possibly others) -g — go to AZ/EL (aborts on new input) -q — exit current submenu -odu — tunnel to outdoor unit (Trav'ler Pro only) -os — enter OS submenu - tasks — list running tasks - kill — kill a named task (e.g. "kill Search") -ngsearch — enter search submenu (HAL 2.05 only) - s — stop search +? — list available commands (alias: help) +command — undocumented (accepts input, purpose unknown) +a3981 — enter motor driver submenu +adc — enter ADC submenu +dipswitch — enter dipswitch submenu +dvb — enter DVB tuner submenu +eeprom — enter EEPROM submenu +gpio — enter GPIO submenu +latlon — enter lat/lon calculator submenu +mot — enter motor control submenu nvs — enter non-volatile storage submenu - d — dump all values (confirmed on Pro and G2) - d — dump single value with name/current/saved/default - e — read NVS value - e — write NVS value - s — save changes -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) +os — enter OS submenu +peak — enter peak/DiSEqC switch submenu +step — enter stepper motor submenu +q — terminate shell (WARNING: kills UART, requires power cycle!) reboot — reboot firmware stow — fold dish flat (caution: modified feeds may not survive) +odu — tunnel to outdoor unit (Trav'ler Pro only) +ngsearch — enter search submenu (HAL 2.05 only) +``` + +#### A3981 Submenu (A3981>) — Allegro Stepper Driver + +``` +reset — reset Az/El A3981 fault flags +diag — read AZ/EL diagnostic status (OK / fault) +cm — Hi/Lo current control (torque) mode +help / ? — list available commands +q — return to TRK> +``` + +#### ADC Submenu (ADC>) — Analog-to-Digital Converter + +``` +m — monitor RSSI (streaming, interrupt with q) +rssi — read RSSI (single-shot, returns raw ADC value) +scan — position sweep with RSSI readings (AZ/EL + lock + SNR) +help / ? — list available commands +q — return to TRK> +``` + +#### DIPSWITCH Submenu (DIPSWITCH>) + +``` +dipswitch — read interpreted dipswitch value +help / ? — list available commands +q — return to TRK> +``` + +#### DVB Submenu (DVB>) — BCM4515 Tuner + +``` +agc — stream RF/IF AGC + SNR + NID (continuous, interrupt with q) +config — BCM hardware/firmware version +diag — multi-block per-transponder diagnostics +dis — display channel parameters (frequency, symbol rate, LNB polarity) +e — edit channel parameter +freqs — tuner frequency list +h — select transponder by ID (1-13) +help / ? — list available commands (first page) +lnbdc odu — enable LNA in ODU mode (13V = V-pol; boot default 18V = H-pol) +lnbv — stream LNB voltage readings (continuous, interrupt with q) +ls — lock status +man — extended help (srch_mode, stats, t, etc.) +qls — quick lock status +rssi — RSSI averaged over n samples (bounded, returns avg + cur) +snr — SNR level (streaming) +srch_mode — auto search mode (from man page) +stats — satellite read stats (from man page) +t — select transponder +table — generate transponder table +di2id — DiSEqC read LNB hardware ID +di2stat — DiSEqC read LNB status flags +send — raw DiSEqC packet (max 6 bytes, space-delimited hex) +q — return to TRK> +``` + +#### EEPROM Submenu (EEPROM>) + +``` +ee [] — read/write EEPROM value at index +inv [] — EEPROM inventory (from help) +def — restore defaults (from help) +help / ? — list available commands +q — return to TRK> +``` + +#### GPIO Submenu (GPIO>) + +``` +dir — set GPIO pin direction +r — read GPIO pin (returns e.g. "B0 = 1") +w — write GPIO pin (requires parameters) +help / ? — list available commands +q — return to TRK> +``` + +#### LATLON Submenu (LATLON>) + +``` +l — calculate lat/lon position (requires 4 parameters) +help / ? — list available commands +q — return to TRK> +``` + +#### MOT Submenu (MOT>) — Motor Control + +``` +a — show position: Angle[0] (AZ), Angle[1] (EL) +a — move motor to absolute angle (0=AZ, 1=EL) +a +/-deg — relative move (G2 only, undocumented) +azscan — scan AZ from EL min to max (from help, untested) +e — engage motors (energize steppers) +g — go to AZ/EL (aborts on new input) +h — home motor to reference position +l — list motors and state (0=AZIMUTH, 1=ELEVATION) +ma — read max acceleration per motor +p — read raw step positions +r — release motors (de-energize steppers) +sd — stall detection test (motor, direction, timeout) +sw — undocumented (requires parameters) +v — read motor velocities +w — undocumented (requires parameters) +help / ? — list available commands +q — return to TRK> +``` + +#### NVS Submenu (NVS>) — Non-Volatile Storage + +**Caution:** NVS `e ` writes values. Any unrecognized input is treated +as a sequential index read (no error string), which generates false positives during +probing but is harmless. `s` saves pending changes to flash. + +``` +d — dump all NVS values (name/current/saved/default) +d — dump single value with details +e — read NVS value at index +e — write NVS value at index (NOT saved until `s`) +s — save pending changes to flash +help / ? — list available commands +q — return to TRK> +``` + +#### OS Submenu (OS>) + +``` +id — full MCU/firmware identification (NVS version, System ID, chip) +reboot — reboot microcontroller +tasks — list running tasks (HAL 0.0.00 only, not on G2) +kill — kill a named task (HAL 0.0.00 only, not on G2) +help / ? — list available commands +q — return to TRK> +``` + +#### PEAK Submenu (PEAK>) — Signal Peak / DiSEqC Switch + +``` +ts — EchoStar switch toggle status +pw — peak signal (from help, details truncated) +help / ? — list available commands +q — return to TRK> +``` + +#### STEP Submenu (STEP>) — Low-Level Stepper Control + +``` +e — engage motor (same as MOT `e`) +ma — set/read max acceleration +p — read step positions (raw counts, not degrees) +r — release motor (same as MOT `r`) +v — read velocity (raw, not degrees/sec) +help / ? — list available commands +q — return to TRK> ``` ### Known NVS Indices diff --git a/docs/g2-nvs-dump.md b/docs/g2-nvs-dump.md index e868812..038409c 100644 --- a/docs/g2-nvs-dump.md +++ b/docs/g2-nvs-dump.md @@ -1,9 +1,99 @@ -# Carryout G2 NVS Dump +# Carryout G2 Firmware Exploration **Firmware:** Version 02.02.48 (Copyright 2013 - Winegard Company) **Date:** 2026-02-12 **Connection:** DSD TECH SH-U11 USB RS-422 @ 115200 8N1 +## Hardware Platform + +Discovered via `os` → `id` command: + +``` +NXP Kinetis K60 ARM Cortex-M4 + Package: 144-pin + Silicon Rev: 2.4 + Mask Set: 4N22D + P-Flash: 512 KB + RAM: 128 KB + Core Clock: 96 MHz (CCLK) + Bus Clock: 48 MHz (BCLK) + System ID: TWELINCH + Antenna ID: 12-IN G2 + Software: 02.02.48 + Flash Base: 0x00010000 (65536) + Flash Size: 458752 bytes (448 KB) +``` + +The "TWELINCH" system ID = "Twelve Inch", matching the Carryout G2's ~12" dish +diameter. Flash starts at 64 KB offset (first 64 KB is bootloader/vector table), +leaving 448 KB for application firmware. + +**Exact part number:** MK60DN512VLQ10 +- MK60 = Kinetis K60 family (Cortex-M4 + DSP) +- DN512 = 512 KB program flash (no FlexNVM) +- VLQ = LQFP 144-pin package (20x20mm) +- 10 = 100 MHz max frequency + +**Datasheets:** `docs/K60-datasheet.pdf` (K60P144M100SF2V2, Rev 3, 6/2013), +`docs/K60-reference-manual.pdf` (K60P144M100SF2V2RM, ~1800 pages) + +### Key Peripherals (from datasheet) + +| Peripheral | Count | Notes | +|------------|-------|-------| +| UART | 5 | UART0 = RS-422 console (confirmed) | +| DSPI | 3 | SPI with DMA; DSPI0/1 likely → A3981 motor drivers | +| I2C | 2 | | +| ADC | 2× 16-bit | 863ns conversion; ADC0 likely → RSSI measurement | +| DAC | 2× 12-bit | | +| USB | 1× OTG | On-chip transceiver, no external PHY needed | +| CAN | 2 | Likely unused | +| Ethernet | 1× IEEE 1588 | Likely unused | +| FlexTimer | 3 (12 ch) | Motor PWM / step timing | +| DMA | 16 channel | | + +### USB Port (Potentially Accessible) + +The K60 has **dedicated USB pins** (not muxable with GPIO): + +| LQFP Pin | Signal | Function | +|----------|--------|----------| +| 19 | USB0_DP | USB Data+ | +| 20 | USB0_DM | USB Data- | +| 21 | VOUT33 | USB VREG 3.3V output | +| 22 | VREGIN | USB VREG 5V input (self-power from USB) | + +The Trav'ler Pro uses USB A-to-A (`ttyACM0`) for its serial console — this +proves Winegard has USB CDC/ACM firmware for the Kinetis platform. The G2 may +also have a USB connector on the PCB (possibly internal, for field service). + +NVS indices 2 ("Debug 2nd Console Port") and 4 ("Debug Port Connection") hint +at multiple console port support — USB could be the second port. + +## Root Menu Structure + +At the `TRK>` prompt, `?` lists all available submenus: + +| Submenu | Command | Description | +|---------|---------|-------------| +| A3981 | `a3981` | Allegro A3981 stepper motor driver IC control | +| ADC | `adc` | Analog-to-digital converter / RSSI / board ID | +| Dipswitch | `dipswitch` | DIP switch configuration readout | +| DVB | `dvb` | BCM4515 DVB receiver / signal analysis | +| EEPROM | `eeprom` | Non-volatile EEPROM storage (separate from NVS) | +| GPIO | `gpio` | MCU pin register dump (5 ports, 98 pins) | +| LATLON | `latlon` | Satellite longitude/elevation parameters | +| MOT | `mot` | Degree-based motor positioning (high-level) | +| NVS | `nvs` | Non-volatile settings (operational parameters) | +| OS | `os` | Operating system / task manager / MCU identification | +| PEAK | `peak` | Signal peaking / DiSEqC switch testing | +| STEP | `step` | Raw microstep motor control (low-level) | + +Three-layer motor control architecture: +1. **`step`** — raw microstep commands (ustep/sec, engage/release motors) +2. **`mot`** — degree-based positioning (`a `, `h `) +3. **Application** — satellite tracking (NVS config, peak, DVB) + ## NVS Values ``` @@ -105,6 +195,27 @@ Num Name Current Saved Default ## Boot Sequence Observed +### Bootloader Phase (<50ms, non-interactive) + +Captured via `scripts/boot_capture.py` with high-resolution timestamps: + +``` +[0.050s] 01 00 ← binary status bytes (bootloader→app handshake?) +[0.050s] Bootloader version: 1.01 +[0.050s] Application is running... +[0.050s] 98 80 96 ← binary bytes (integrity check? jump address?) +[0.100s] Application Starting Kinetis PCB... power up/reset +``` + +The bootloader runs at **115200 baud** (same as application — confirmed by +multi-baud capture at 9600/19200/38400/57600/230400/460800). There is **no +interactive window** — ESC, CR, BREAK, 0x55 autobaud, and other interrupt +sequences at 5-30ms delays all failed to stop the boot. The bootloader +checks a flag (likely in EEPROM or a reserved flash sector) and immediately +jumps to the application at 0x10000 if no firmware update is pending. + +### Application Phase (~10s to prompt) + ``` Version 02.02.48 Copyright 2013 - Winegard Company @@ -191,6 +302,145 @@ direction of travel, undershooting on return. This is classic stepper backlash + PID settling behavior and is what the leapfrog algorithm compensates for. +### MOT Submenu — Full Command Reference + +``` +Available commands: + a Go to angle [[[motor] [[+|-]angle]]] + azscan Scan AZ from EL Min-Max Angle [az_rel_angle] [el_rel_angle] [delay] + azscanwxp Scan AZ from EL Min-Max with all transponders [motor] [span] [resolution] [num_xp] + e Engage motors + ela2s Elevation Law Test - Angle to Steps [angle] + elminmaxhome Display Min, Max & Home Elevation Angles + els2a Elevation Law Test - Steps to Angle [steps] + h Home Motors [motor num or * (both)] + l List Motors in System + life Az/El Life test [az_rel_angle] [el_rel_angle] + ma Set Max Acceleration [[motor] [deg/sec/sec]] + motorboth Both Motor Life test [AZ delta(0-25)] [EL delta(0-25)] + motorlife Motor Life Test [motor_id] [min_angle] [max_angle] + mv Set Max Velocity [motor] [deg/sec] + p Go To Position [motor] [pos] + pid Set PID Parameters [motor] [Kp] [Kv] [Ki] + r Release Motors + sd Stall Detect [motor] [dir] [timeout_ms] [iterations (0=forever)] + sp Set Position [motor] [pos] + sw Set Wrap Position [motor] [pos] + v Goto Velocity [motor] [deg/rev] + vms Goto Velocity For Milliseconds [motor] [deg/rev] [ms] + w Wrap Manager [motor] [ON/OFF] +``` + +### Relative Moves + +`a` supports `+`/`-` prefix for **relative** moves: + +``` +a 0 +5 ← move AZ 5° CW from current +a 1 -2 ← move EL 2° down from current +``` + +This is undocumented in the upstream repos and very useful for incremental +positioning during tracking. + +### Motor List + +``` +l +Motors: + 0 - AZIMUTH: local + 1 - ELEVATION: local +``` + +"local" means direct A3981 driver control (vs. a networked motor controller). + +### Motor Dynamics + +``` +ma → Accel[0] = 400.0 Accel[1] = 400.0 (deg/sec²) +mv → Max Vel[0] = 65.0 Max Vel[1] = 45.0 (deg/sec) +``` + +Both axes have identical acceleration (400 deg/sec²). AZ max velocity is +faster (65 deg/sec) than EL (45 deg/sec) — different gear ratios and +mechanical loads. + +### Step Position + +`p` shows position in microsteps: + +``` +p → Position[0] = 19998 Position[1] = 3116 +``` + +Cross-check with angle: AZ 179.98° × (40000/360) = 19998 steps. Linear +mapping for both axes (angle = steps × 360 / steps_per_rev). + +### Elevation Limits + +``` +elminmaxhome → Min: 1800 Max: 6500 Home: 6500 +``` + +All values in centidegrees: **Min=18.00°, Max=65.00°, Home=65.00°**. The +home position is at maximum elevation (stow position). + +### Elevation Law Conversion + +`ela2s` converts angle → steps, `els2a` converts steps → angle: + +| Angle | Steps | Notes | +|-------|-------|-------| +| 0° | 1248 | Below min (warning: "Min: 1800") | +| 18° | 1248 | Minimum EL (same steps as 0°) | +| 45° | 3120 | | +| 65° | 4506 | Maximum EL | +| 90° | 4506 | Above max (warning: "Max: 6500") | + +The mapping is linear: steps ≈ angle × (24960/360). The "law" is a simple +linear function for this dish — no non-linear linkage compensation. + +### Engage / Release + +- `e` — Engage motors (enable A3981 drivers, PID loop holds position) +- `r` — Release motors (disable drivers, dish can move freely by hand) + +### Sky Scan Commands + +**`azscan `** — Scan AZ while stepping EL from +min to max angle. Parameters are relative angles and dwell delay. Used for +basic signal surveys. + +**`azscanwxp `** — Advanced sky scan +that steps in hundredths of a degree and checks all transponders at each +position. This is the core of Davidson's winegard-sky-scan project. The +`resolution` parameter in hundredths of a degree enables 0.01° precision +scanning — far finer than the standard `a` command. + +### Stall Detection + +`sd [iterations]` — Run stall detection on a +motor. Default timeouts: AZ=10000ms, EL=2000ms. The firmware drives the +motor in the specified direction until it stalls (current spike from A3981). +Set iterations to 0 for continuous mode. Used during homing and calibration. + +### Life / Durability Tests + +Factory test commands that continuously exercise the motors: + +- `life ` — Oscillate both axes by relative angles +- `motorlife ` — Sweep a single motor between min/max angles +- `motorboth ` — Exercise both motors, max 25° delta each + +### Write-Only Commands + +These commands require parameters — no read-only mode: + +- `pid ` — Set PID gains (no read command) +- `sp ` — Set step position counter (doesn't move motor) +- `sw ` — Set wrap position +- `w ON/OFF` — Enable/disable wrap manager + ## DVB Subsystem (BCM4515) ### Hardware @@ -265,28 +515,312 @@ Reads:1 RF_AGC[avg: 1327353088 cur: 1327353088] IF_AGC[avg: 2684354560 cur: 2 - SNR: 0.0 when no signal lock - NID: FFFF/none = no DVB network ID detected +### SNR (Streaming) + +`snr` streams signal-to-noise ratio readings: + +``` +Reads:1 SNR[avg: 0.0 cur: 0.0] +Reads:2 SNR[avg: 0.0 cur: 0.0] +... +Reads:223 SNR[avg: 0.0 cur: 3.1] ← transient RF spike +Reads:250 SNR[avg: 0.0 cur: 2.6] ← another transient +``` + +At noise floor, average stays 0.0 but occasional transient spikes appear in +the `cur` field — fleeting RF energy. Stays in DVB submenu when interrupted. + +### Lock Status (`ls`) — Streaming + +`ls` is a STREAMING command that continuously scans all 32 transponders, trying +multiple modulations per frequency. Output is a continuous scan log: + +``` +Xp:1 Freq:974000 SymRate:20000 Mode:blind_scan ... no_lock +Xp:1 Freq:974000 SymRate:20000 Mode:turbo_qpsk_... ... no_lock +... +``` + +**When interrupted with CR, `ls` prints "Terminating shell." and drops to +`TRK>` (exits DVB submenu entirely).** This is unique among DVB streaming +commands — all others stay in DVB submenu when interrupted. + +### Quick Lock Status (`qls`) — Streaming + +`qls` streams compact lock status at ~100ms intervals: + +``` +Lock:0 rssi:500 cnt:0 +Lock:0 rssi:500 cnt:0 +... +``` + +Stays in DVB submenu when interrupted. Ideal for real-time signal monitoring +during dish movement (low bandwidth, high update rate). + +### Network ID (`nid`) — Streaming + +`nid` streams network identification: + +``` +nid: FFFF/none +nid: FFFF/none +``` + +Uses CR-overwrite (carriage return without newline) for in-place updates. +FFFF = no DVB network detected. Can be difficult to interrupt cleanly — +may need multiple CR + flush cycles. + +### Signal Statistics (`stats`) — Streaming + +`stats` streams signal statistics. Produces no output when there is no signal +lock. Stays in DVB submenu when interrupted. + +### Diagnostics (`diag`) — One-Shot + +`diag` is a multi-block ONE-SHOT command that outputs detailed per-transponder +diagnostics for each transponder currently being tried: + +``` + SymRate: 18514984 Freq: 974000 + Bit Rate: 29893160 + SNR: 0.0 SymRateErr: -1258 CarrierOffset: 0 CarrierErr: 3295097 + Tuner LPF: 12 RF_AGC: 2214707264 + BER Errors: 0 MPEG Frm Errors: 0 MPEG Frm Count: 0 + Reacquisitions: 7 + RS Corr/Uncorr: 0 / 0 Pre-Vit: 0 + sp inv: scan phase rotation: 0 + Acq Time: 4259 msec + trans_mod_coderate: no_lock + Tuner PLL: LOCKED Internal BERT: not locked + Demod: not locked Timing Loop Lock: disabled +``` + +### Transponder Table (`table`) — One-Shot (Long) + +`table` generates a full scan across all 32 transponders. Takes ~136 seconds +(~4.25s per transponder, matching `tabto` acquisition timeout of 4000ms). + +The output starts with a configuration summary showing all modifiable parameters +with their current values and the commands to change them, then a detailed +per-transponder table with columns: + +``` +Xp Freq SigDet SymRate PeakPower SNR LPF RF_AGC AcqTime Mode +NID/SAT XpTime LNBVsens RSSI BitrateOut SymRateError CarrierOffset +CarrierError BER_Errors MPEG_Errors MPEG_Count Reacq +``` + +With shuffle mode ON, transponders scan in interleaved groups of 4: +1,2,3,4 → 17,18,19,20 → 5,6,7,8 → 21,22,23,24 → 9,10,11,12 → 25,26,27,28 +→ 13,14,15,16 → 29,30,31,32. + +LNB voltage alternates between ~13V (vertical polarization) and ~20V +(horizontal polarization) across transponder groups. + +After scan completes: `Table Completion Time (seconds): 136 Transponders Locked: 0` +then auto-restores LNB to STB mode: `Enabled LNB STB`. + +### Frequency List (`freqs`) + +Returns the active frequency list name: + +``` +freqs → Non-Stacked +``` + +"Non-Stacked" means standard Ku-band IF frequencies without stacking +(stacked LNBs combine multiple bands into one cable). + +### Transponder Range (`range`) + +``` +range → Transponder Range: [ 1 - 32 ] +``` + +### Power State (`pwr`) + +``` +pwr → SDS and DiSEqC core power enabled +``` + +Shows power state of the Satellite Detection System and DiSEqC subsystems. + +### Search Mode (`srch_mode`) + +``` +srch_mode → Auto Search Mode: 1 +``` + +Mode 1 = automatic transponder search with blind scan. + +### Timeout Settings + +Two separate timeout configurations: + +**Table scan timeouts (`tabto`):** +``` +Timeout (msec): Acq:4000 NID:20000 Signal Detect:0 +``` + +**Single transponder tune timeouts (`to`):** +``` +Timeout (msec): Acq:500 NID:12000 +``` + +`tabto` takes arguments: `tabto ` (set 0 to disable NID/SD). +`to` takes arguments: `to `. + +### Toggle Commands + +These commands toggle a mode and print the new state: + +| Command | What it toggles | +|---------|----------------| +| `srch` | Search Transponders mode (on/off) | +| `shuf` | Transponder shuffle order (on/off) | +| `tablex` | Extended table mode (on/off) | + +### Defaults Reset (`def`) + +`def` silently resets all channel parameters to defaults. **No output, no +confirmation.** Use with caution — there is no undo. + +### Modulation Switch (`msw`) + +`msw` requires arguments but rejects all tested inputs (`msw 0`, `msw 1`, +`msw on`). Format unknown — possibly vestigial or requires a specific +modulation string. + +### Channel Parameter Help (`h `) + +`h ` shows valid values for each of the 13+2 channel parameters: + +| Param | Name | Range / Values | +|-------|------|---------------| +| 1 | Frequency | 250000–2150000 kHz (L-band IF) | +| 2 | Symbol Rate | 0 (Peak Scan) or 2000–45000 ksps | +| 3 | Trans_Mod_CRate | 30+ modes (see table below) | +| 4 | Blind Scan Mode | Bitmask: s2, trb, dvb, dss, dcii (see below) | +| 5 | LNB Polarity | 13V (vertical) / 18V (horizontal) | +| 6 | LNB Tone | off / on (22kHz for high-band switching) | +| 7 | Roll-off | 0.20 / 0.35 | +| 8 | LPF Cutoff | 0 (auto) or 1–40 MHz | +| 9 | Carrier Offset | -5000 to +5000 Hz | +| 10 | FreqSearchRange | 0–10000 kHz | +| 11 | DCII Mode | comb, i, q (QPSK) / ocomb, oi, oq (OQPSK) | +| 12 | Spectral Inversion | 0=normal, 1=q inv, 2=i inv, 3=scan | +| 13 | PScnSymRtRngMin | 2000–45000 ksps | +| 14 | PScnSymRtRngMax | (shown in `dis`, no `h 14` help) | +| 15 | SignalDetectMode | (shown in `dis`, no `h 15` help) | + +#### Supported Modulations (Param 3) + +``` +DVB-S: dvbs_qpsk_1_2, dvbs_qpsk_2_3, dvbs_qpsk_3_4, + dvbs_qpsk_5_6, dvbs_qpsk_7_8 +DSS: dss_qpsk_1_2, dss_qpsk_2_3, dss_qpsk_6_7 +DCII: dcii_qpsk_1_2, dcii_qpsk_2_3, dcii_qpsk_3_4, + dcii_qpsk_5_11, dcii_qpsk_4_5 +DVB-S2: s2_qpsk_1_2, s2_qpsk_3_5, s2_qpsk_2_3, s2_qpsk_3_4, + s2_qpsk_4_5, s2_qpsk_5_6, s2_qpsk_8_9, s2_qpsk_9_10, + s2_8psk_3_5, s2_8psk_2_3, s2_8psk_3_4 +Turbo: turbo_qpsk_1_2, turbo_qpsk_2_3, turbo_qpsk_3_4, + turbo_qpsk_5_6, turbo_qpsk_7_8, + turbo_8psk_2_3, turbo_8psk_3_4, turbo_8psk_4_5, + turbo_8psk_5_6, turbo_8psk_8_9 +Special: blind_scan +``` + +#### Blind Scan Mode Bitmask (Param 4) + +Visual bitmask format: `s2_trb_dvb_dss_dcii` + +| Value | Pattern | Standards enabled | +|-------|---------|-------------------| +| `s2` | `s2_______________` | DVB-S2 only | +| `trb` | `___trb___________` | Turbo FEC only | +| `dvb` | `_______dvb_______` | DVB-S only | +| `dss` | `___________dss___` | DSS (DirecTV) only | +| `dcii` | `_______________dcii` | DCII (DigiCipher) only | +| `all` | `s2_trb_dvb_dss_dcii` | All standards | +| `dish` | `___trb_dvb________` | DISH Network (turbo + DVB-S) | +| `3` | `___trb_dvb_dss___` | DirecTV 3-standard | +| `shaw` | `___trb_________dcii` | Shaw (turbo + DCII) | +| `nodcii` | `s2_trb_dvb_dss___` | All except DCII | +| `nos2` | `___trb_dvb_dss_dcii` | All except DVB-S2 | + +### DiSEqC Commands + +All DiSEqC 2.x read commands fail with `RxReplyTimeout` when no switch is +connected (expected — the G2 has a direct LNB without a multi-switch): + +| Command | Function | Result (no switch) | +|---------|----------|--------------------| +| `di2id` | Read LNB hardware ID | `LNB Read HW ID FAIL` | +| `di2stat` | Read LNB status | `LNB Read Status FAIL` | +| `di2conf` | Read LNB config | `LNB Read Config FAIL` | +| `di2sc` | Short circuit test | `LNB Short Circuit FAIL` | +| `di2rcs` | Read switch state | `LNB Switch State FAIL` | +| `di2cs` | Configure switch | `No Parameters Specified` (needs args) | +| `send <3-6 bytes>` | Raw DiSEqC packet | (not tested — no switch) | + +### DiSEqC Timing Parameters + +| Command | Parameter | Default | +|---------|-----------|---------| +| `ovraddr` | LNB address | 0x11 (standard first LNB) | +| `rrto` | Receive reply timeout | 210 ms | +| `tdthresh` | Tone detect threshold | 110 (units: 0.16 counts/mV) | +| `pretx` | Pre-command TX delay | 15 ms | + ### 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) | +| `snr` | Streaming | SNR readings (avg + current) | +| `agc` | Streaming | RF/IF AGC + SNR + NID | | `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 | +| `ls` | Streaming | Full transponder lock scan (exits DVB on interrupt!) | +| `qls` | Streaming | Quick lock status (~100ms updates) | +| `nid` | Streaming | Network ID (CR-overwrite display) | +| `stats` | Streaming | Signal statistics (silent when no lock) | | `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) | +| `dis` | One-shot | Display all channel parameters | +| `diag` | One-shot | Multi-block per-transponder diagnostics | +| `table` | One-shot | Full 32-transponder scan (~136s) | +| `freqs` | One-shot | Frequency list name | +| `range` | One-shot | Transponder scan range | +| `pwr` | One-shot | SDS/DiSEqC power state | +| `srch_mode` | One-shot | Auto search mode value | +| `tabto` | Read/Write | Table scan timeouts (acq/nid/sd) | +| `to` | Read/Write | Single tune timeouts (acq/nid) | +| `t ` | Write | Select transponder | +| `e ` | Write | Edit channel parameter | +| `h ` | Read | Parameter help (valid values for param n, 1-13) | +| `srch` | Toggle | Search transponders mode | +| `shuf` | Toggle | Transponder shuffle order | +| `tablex` | Toggle | Extended table mode | +| `def` | Write | Reset all params to defaults (silent, no undo!) | +| `msw ` | Write | Modulation switch (format unknown) | +| `ovraddr [addr]` | Read/Write | DiSEqC LNB address | +| `rrto [ms]` | Read/Write | DiSEqC receive reply timeout | +| `tdthresh [val]` | Read/Write | DiSEqC tone detect threshold | +| `pretx [ms]` | Read/Write | DiSEqC pre-TX delay | +| `di2id` | Read | DiSEqC 2.x: read LNB hardware ID | +| `di2stat` | Read | DiSEqC 2.x: read LNB status | +| `di2conf` | Read | DiSEqC 2.x: read LNB config | +| `di2sc` | Read | DiSEqC 2.x: short circuit test | +| `di2rcs` | Read | DiSEqC 2.x: read switch state | +| `di2cs ` | Write | DiSEqC 2.x: configure switch | +| `send <3-6 hex>` | Write | Raw DiSEqC packet | -Streaming commands (`agc`, `lnbv`) run until a new command or `q` interrupts them. +**Streaming commands:** `snr`, `agc`, `lnbv`, `qls`, `nid`, `stats` run until +CR interrupts. All stay in DVB submenu except `ls` which drops to `TRK>`. + +**Toggle commands:** `srch`, `shuf`, `tablex` alternate on/off and print new state. ## Satellite Configuration @@ -297,3 +831,877 @@ Alternate Update: 11900 ← 119.00° Sat Provider Update: 1 ← Provider ID Dipswitch Value: 101 ← DirecTV configuration ``` + +## A3981 Motor Driver IC + +The Allegro A3981 is an automotive-grade programmable stepper motor driver +controlled by the K60 MCU via SPI. Two A3981 chips — one per axis (AZ, EL). + +**Datasheet:** `docs/A3981-datasheet.pdf` (Allegro Microsystems) +**ECAD files:** `docs/A3981-ecad.kicad_sym`, `docs/A3981-ecad.pretty/` + +### A3981 Commands (`A3981>`) + +| Command | Type | Description | +|---------|------|-------------| +| `diag` | Read | Fault diagnostics for both axes | +| `sm` | Read | Step size mode (AUTO/MANUAL) | +| `ss` | Read | Current step size (microstepping level) | +| `cm` | Read | Current control mode (AUTO/MANUAL) | +| `st ` | Write | Set torque/current parameters | +| `reset` | Write | Clear latched A3981 faults on both axes | + +### Diagnostics + +``` +diag +AZ DIAG: OK +EL DIAG: OK +``` + +### Microstepping Configuration + +``` +ss +KEY: FULL-16, HALF-8, QTR-4, EIGHTH-2, SIXTEENTH-1 +AZ Step Size:1 +EL Step Size:1 +``` + +Both axes at step size 1 = **1/16 microstepping** (finest available). The A3981 +supports full, half, quarter, eighth, and sixteenth steps. The inverted key +(FULL=16, SIXTEENTH=1) is the firmware's internal representation — likely a +divisor applied to the full-step pulse count. + +### Step Size and Current Modes + +``` +sm cm +AZ Step Size Mode = AUTO AZ: Mode = AUTO +EL Step Size Mode = AUTO EL: Mode = AUTO +``` + +AUTO mode means the driver dynamically adjusts microstepping resolution and +current level based on motor speed. At low speeds, fine microstepping (1/16) +provides smooth motion and precise positioning. At higher speeds, the driver +may switch to coarser steps to maintain torque. + +### Fault Reset + +``` +reset +Az/El A3981 Faults Reset. +``` + +Clears latched fault conditions (overcurrent, open load, thermal). This is a +**write** operation — it actively clears the fault registers, not a read-only +status check. Use `diag` for non-destructive fault checking. + +## ADC Subsystem (`ADC>`) + +| Command | Type | Description | +|---------|------|-------------| +| `m` | Streaming | Monitor RSSI continuously | +| `rssi` | One-shot | Read RSSI (same noise-floor value as DVB) | +| `scan` | Unknown | Scan ADC on azimuth axis | +| `bdid` | One-shot | Board identification string | +| `bdrevid` | One-shot | Board revision ID | + +### Board Identification + +``` +bdid → STATIONARY +bdrevid → A +``` + +"STATIONARY" confirms this is the non-mobile (non-in-motion) variant. Board +revision "A" is the first production revision. + +### RSSI via ADC + +``` +rssi → 500 +``` + +Same noise floor value (500) as the DVB `rssi` command. The ADC subsystem reads +the same analog signal path — likely a baseband power detector output from the +BCM4515 routed to a K60 ADC input. + +## Dipswitch (`DIPSWITCH>`) + +``` +dipswitch +val:ffffff01 +app_dipswitch:101 +``` + +The raw value `0xFFFFFF01` has the low byte = 0x01. The `app_dipswitch` value +101 corresponds to the DirecTV satellite configuration. Dipswitch values select +the satellite provider (DirecTV, DISH, Bell) and determine which transponder +frequencies the search algorithm tries. + +## EEPROM (`EE>`) + +| Command | Type | Description | +|---------|------|-------------| +| `ee ` | Read | Read EEPROM value at index | +| `ee ` | Write | Write EEPROM value at index | +| `inv []` | Write | **Invalidate** EEPROM index (destructive!) | +| `def` | Write | Restore all EEPROM values to factory defaults | + +EEPROM is a separate storage area from NVS. NVS holds operational parameters +(motor tuning, PID gains, satellite config). EEPROM appears to hold calibration +or factory data — likely stored in an external I2C EEPROM (AT24Cxx or similar) +rather than the K60's internal flash. + +### EEPROM Structure + +The EEPROM has exactly **17 indices (0-16)**. The firmware enforces bounds: + +``` +ee 17 +Index out of bounds:17 Min:0 Max:16 +``` + +Each index stores a 32-bit unsigned integer. Invalid entries return a sentinel +value of **65793 (0x00010101)** — three bytes of 0x01 in the low 24 bits. The +firmware distinguishes between valid and invalid reads: + +- **Valid:** `Read value = ` — data is trusted +- **Invalid:** `Failed to read. val:` — data exists but flagged invalid + +### Complete EEPROM Dump + +Dumped via `scripts/ee_dump.py` (pyserial script scanning all indices): + +| Index | Decimal | Hex | Status | Notes | +|------:|--------:|-----|--------|-------| +| 0 | 65793 | 0x00010101 | INVALID | Accidentally invalidated (`inv 0`) | +| 1 | 0 | 0x00000000 | OK | | +| 2 | 22897 | 0x00005971 | OK | | +| 3 | 3748 | 0x00000EA4 | OK | | +| 4 | 4346 | 0x000010FA | OK | | +| 5 | 11637 | 0x00002D75 | OK | | +| 6 | 65793 | 0x00010101 | INVALID | Factory default (never written) | +| 7 | 65793 | 0x00010101 | INVALID | Factory default | +| 8 | 65793 | 0x00010101 | INVALID | Factory default | +| 9 | 65793 | 0x00010101 | INVALID | Factory default | +| 10 | 65793 | 0x00010101 | INVALID | Factory default | +| 11 | 65793 | 0x00010101 | INVALID | Factory default | +| 12 | 65793 | 0x00010101 | INVALID | Factory default | +| 13 | 65793 | 0x00010101 | INVALID | Factory default | +| 14 | 65793 | 0x00010101 | INVALID | Factory default | +| 15 | 65793 | 0x00010101 | INVALID | Factory default | +| 16 | 65793 | 0x00010101 | INVALID | Factory default | + +Only **6 indices** (0-5) were ever written with valid data. Indices 6-16 have +never been programmed — all contain the 0x00010101 sentinel. The EEPROM has +capacity for 17 values but only the first 6 are used by this firmware version. + +### Value Analysis + +The valid EEPROM values (indices 1-5) don't correspond to any obvious motor +positions, NVS indices, or physical constants. They may represent: + +- **Factory calibration offsets** — motor encoder corrections, sensor trim +- **Manufacturing data** — serial number components, PCB revision, test results +- **Checksum/signature** — authentication for firmware/hardware pairing + +Index 0 (originally valid, now invalidated) was likely a calibration value or +header byte. Its original value is unknown — `def` might restore it. + +### Command Notes + +**`inv []`** — "Invalidate", not "inventory". Marks an index as invalid +by writing the 0x00010101 sentinel. The firmware still returns the raw value +on read but flags it as `Failed to read`. **Destructive and immediate** — no +confirmation prompt. + +**`def`** — Restores all EEPROM indices to factory defaults. Not tested on this +unit (would overwrite the accidentally-invalidated index 0 but also reset any +user-modified values). Worth trying if index 0's missing value causes issues. + +**`ee `** — Write a value to an index. Accepts decimal integers. +Can be used to restore invalidated indices if the original value is known. + +## GPIO Registers (`GPIO>`) + +| Command | Type | Description | +|---------|------|-------------| +| `regs` | Read | Dump all GPIO pin states (0/1) | +| `r ` | Read | Read single GPIO pin (e.g., `r A 5`) | +| `w ` | Write | Write to GPIO pin (toggle) | +| `dir ` | Write | Set pin direction (input/output) | + +The `regs` command dumps all MCU GPIO pins across 5 ports (K60 144-pin). +Note: pins A20-A23 and B12-B15 are not enumerated (reserved or unbonded): + +| Port | Pins Enumerated | High Pins (=1) | +|------|----------------|----------------| +| A | A0–A19, A24–A29 (26 pins) | A1, A3, A4, A5, A15, A16, A25–A29 | +| B | B0–B11, B16–B23 (20 pins) | B0, B1, B2, B3, B11 | +| C | C0–C19 (20 pins) | C10, C11, C12, C13, C18 | +| D | D0–D15 (16 pins) | D11, D12, D13 | +| E | E0–E12, E24–E29 (19 pins) | E0, E1, E2, E4, E5, E7, E9–E12, E24–E28 | + +Total: 101 pins enumerated. "Unknown bit E29" logged by firmware — pin E29 +is defined in hardware but not assigned a function (test point or reserved). + +## LATLON (`LATLON>`) + +| Command | Type | Description | +|---------|------|-------------| +| `l ` | Read | Calculate dish lat/lon from two satellite observations | + +This is the dish's **self-localization algorithm** — it triangulates its own +geographic position by observing two known geostationary satellites. The four +parameters are longitude and elevation pairs for two satellites. + +``` +l -110 -119 40 38 + anglesentered = -11000 -11900 4000 3800 + Lat = 4295 Lon = 25655 +``` + +Output is in centidegrees: Lat 4295 = 42.95°N, Lon 25655 = 256.55°E (= 103.45°W). +This is a reasonable US mid-latitude position given DISH Network satellites at +110°W and 119°W with elevation angles of 40° and 38°. + +The firmware uses this for automatic satellite look-angle computation when no +GPS is available (the G2 has no GPS module — "GPS Not Found" at boot). + +## PEAK Subsystem (`PEAK>`) + +| Command | Type | Description | +|---------|------|-------------| +| `ts` | **Streaming (DANGEROUS)** | EchoStar/DiSEqC switch toggle — runs forever, can't be interrupted | +| `pw ` | Motor+Signal | Peak wide — sweep around AZ/EL to find signal peak | +| `psnr ` | Motor+Signal | Peak SNR — sweep around AZ/EL optimizing for SNR | +| `pxy1 []` | Motor+Signal | Peak XY1 — 2D cross-pattern peak search, repeat n times (default 1) | +| `stb` | One-shot | STB control test — toggles LNB polarity and compares RSSI | +| `rssits` | **Streaming** | RSSI test — prints every 1 minute (exits submenu on interrupt!) | + +### EchoStar Switch Toggle (`ts`) — DANGEROUS! + +The `ts` command runs **indefinitely**, probing for a DiSEqC/EchoStar switch +by toggling LNB voltage and reading the switch response. It cannot be stopped +by sending `q` — the running command consumes all input. The only escape is to +close and reopen the serial port. + +``` +ts +(14000+ reads, all showing: 0b0000 0) +``` + +All reads returned `0b0000 0` — no switch connected (expected, since the G2's +LNB is directly connected without a multi-switch). + +### STB Control Test (`stb`) + +Toggles LNB polarization and compares RSSI at each polarity: + +``` +stb +Enabled LNB ODU 18V +Even_sig = 504 +Enabled LNB STB +Odd_sig = 232 +Enabled LNB STB +Odd_sig = 233 Ctr = 0 +``` + +- `Enabled LNB ODU 18V` → horizontal polarization (18V), `Even_sig = 504` +- `Enabled LNB STB` → vertical polarization (13V), `Odd_sig = 232` +- `Ctr = 0` → no DiSEqC switch response detected +- The 504 vs 232 difference (~2x) reflects the polarization-dependent noise + floor. "Even" = H-pol transponders (18V), "Odd" = V-pol (13V) — DBS convention. + +### RSSI Test (`rssits`) + +Starts a background RSSI monitoring task that prints every 1 minute. When +interrupted, **exits the PEAK submenu entirely** (drops to `TRK>` root), +similar to DVB `ls` behavior. + +## STEP Subsystem (`STEP>`) — Low-Level Motor Control + +| Command | Type | Description | +|---------|------|-------------| +| `e` | Write | Engage motor (enable driver) | +| `r` | Write | Release motors (disable driver, coast to stop) | +| `p` | Read/Write | Go to position (absolute, in microsteps), or read current position | +| `v` | Write | Go to velocity (continuous rotation) | +| `ma` | Read/Write | Set/get max acceleration (ustep/sec/msec) | +| `mv` | Read/Write | Set/get max velocity (ustep/sec) | +| `pid` | Read/Write | Set/get PID tuning values | + +This is the raw stepper motor interface — below the `mot` menu's degree-based +abstraction. Commands operate in **microsteps** rather than degrees. The +relationship between microsteps and degrees depends on: + +- Gear ratio (NVS 49, currently 0x00000000) +- Steps per revolution (NVS 83: AZ=40000, NVS 88: EL=24960) +- Microstepping level (A3981: both at 1/16) + +### Current Values (read mode) + +``` +p → Step Pos[0] = 19998 Step Pos[1] = 3116 +ma → Accel[0] = 44 Accel[1] = 28 +mv → Max Vel [0] = 7222 Max Vel [1] = 3120 +pid → Kp=250 Kv=50 +``` + +### MOT↔STEP Conversion Table + +| Parameter | STEP (microsteps) | MOT (degrees) | Factor | +|-----------|-------------------|---------------|--------| +| AZ position | 19998 | 179.98° | 111.11 steps/° | +| EL position | 3116 | 44.94° | 69.33 steps/° | +| AZ max vel | 7222 ustep/s | 65.0°/s | ÷111.11 | +| EL max vel | 3120 ustep/s | 45.0°/s | ÷69.33 | +| AZ accel | 44 ustep/s/ms | ~396°/s² | ×1000÷111.11 | +| EL accel | 28 ustep/s/ms | ~404°/s² | ×1000÷69.33 | + +### PID Tuning + +`pid` returns `Kp=250 Kv=50` — proportional and velocity gains. No Ki term, +indicating a PD (proportional-derivative) position loop. This is typical for +stepper motors which don't drift under holding torque. Matches NVS indices +128-129. Note: `pid` is write-only in `MOT>` but **readable in `STEP>`**. + +The `e`/`r` commands engage and release the A3981 motor drivers. When released, +the motors are unpowered and the dish can be moved by hand (useful for manual +positioning or emergency stow). When engaged, the PID loop holds position. + +## ADC Subsystem (`ADC>`) + +| Command | Type | Description | +|---------|------|-------------| +| `bdid` | Read | Board ID — returns `STATIONARY` | +| `bdrevid` | Read | Board revision ID — returns `A` | +| `rssi` | Read | Single RSSI reading (ADC value, not DVB RSSI) | +| `m` | Toggle? | Monitor RSSI — returns immediately, may enable background monitoring | +| `scan` | **Streaming** | Continuous ADC scan with position, RSSI, Lock, SNR, and delta | + +### Board Identity + +- **Board ID:** `STATIONARY` — distinguishes from mobile/in-motion antenna variants +- **Board Rev ID:** `A` — PCB revision + +### RSSI (ADC) + +`rssi` returns a single ADC reading: `232` at noise floor. This is the raw +ADC value from an analog RSSI detector (separate from the DVB tuner's digital +RSSI). The ADC RSSI baseline (~232) corresponds to the DVB `Odd_sig` in the +PEAK `stb` test, suggesting both measure the same RF path at V-pol. + +### Scan (Streaming) + +`scan` performs a continuous RF scan at the current position: + +``` +Starting position AZ:17998 EL:4494 Stop at:0 +Motor:0 Angle:17998 RSSI:232 Lock:0 SNR: 0.0 Scan Delta:0 +Motor:0 Angle:17998 RSSI:238 Lock:0 SNR: 0.0 Scan Delta:0 +Motor:0 Angle:17998 RSSI:233 Lock:0 SNR: 0.4 Scan Delta:0 +... +``` + +Positions are in centidegrees (17998 = 179.98°). `Scan Delta:0` indicates no +motor movement — the scan reads RSSI at the fixed position. `Stop at:0` suggests +it scans until manually interrupted. Occasional SNR transients (0.1–0.4 dB) +appear from fleeting RF energy. The scan likely moves the motor when used with +the `azscanwxp` sky-scan command from the MOT menu. + +## Dipswitch Subsystem (`DIPSWITCH>`) + +| Command | Type | Description | +|---------|------|-------------| +| `dipswitch` | Read | Read physical DIP switch state and interpreted value | + +``` +dipswitch +val:ffffff01 +app_dipswitch:101 +``` + +- `val: ffffff01` — raw GPIO register reading. The `0x01` LSB indicates one + switch position is active; `0xFF` bytes are pulled-up (inactive) switches. +- `app_dipswitch: 101` — firmware-interpreted value. Matches NVS index 113 + ("Dipswitch Value: 101"), which encodes a DirecTV satellite configuration. + +The physical DIP switch on the PCB selects the satellite provider's transponder +list for TV search mode. NVS index 112 ("Disable Dipswitch?") controls whether +the firmware reads the physical switch or uses the NVS-stored value. Since +we've disabled the tracker (NVS 20 = TRUE), the DIP switch setting is ignored. + +## A3981 Motor Driver (`A3981>`) + +| Command | Type | Description | +|---------|------|-------------| +| `diag` | Read | Read AZ/EL diagnostic pins (fault status) | +| `sm` | Read/Write | Get/set step size mode (AUTO, manual) | +| `ss` | Read/Write | Get/set step size (microstepping divisor) | +| `st` | Read/Write | Get/set torque level (HIGH/LOW current) | +| `cm` | Read/Write | Get/set current control mode (AUTO, manual) | +| `reset` | Write | Reset AZ/EL A3981 diagnostic/fault registers | + +### Current State + +``` +diag → AZ DIAG: OK EL DIAG: OK +sm → AZ: Step Size Mode = AUTO EL: Step Size Mode = AUTO +ss → AZ: Step Size:1 EL: Step Size:1 +st → AZ Torq:LOW EL Torq:LOW +cm → AZ: Mode = AUTO EL: Mode = AUTO +``` + +### Step Size Key + +The `ss` value is a microstepping **divisor**, not multiplier: + +| Value | Mode | Microsteps per full step | +|-------|------|------------------------| +| 16 | FULL | 1 (full step) | +| 8 | HALF | 2 | +| 4 | QTR | 4 | +| 2 | EIGHTH | 8 | +| 1 | SIXTEENTH | 16 (finest) | + +Both motors are at `1` (1/16 microstepping — finest resolution). In `AUTO` +mode, the A3981 IC automatically selects the step size based on speed: finer +steps at low speed for smooth positioning, coarser steps at high speed for +torque. + +### Torque and Current Control + +`st` shows torque is `LOW` (motors idle/holding). `cm` shows current control +is `AUTO` — the firmware switches between high current (moving) and low current +(holding) automatically. This reduces power consumption and heat when the dish +is stationary. + +The `diag` pins expose A3981 fault conditions: overcurrent, overtemperature, +open-load (disconnected motor winding), or short-to-ground. `OK` means no +faults detected. Use `reset` to clear latched fault flags. + +## OS Subsystem (`OS>`) + +| Command | Type | Description | +|---------|------|-------------| +| `id` | Read | Full MCU and firmware identification | +| `reboot` | Write | Reboot microcontroller (confirmed — full boot cycle ~10s) | + +### System Identification (`id`) + +``` +NVS Version: 1.02.13 +System ID: TWELINCH + K60-144pin + Silicon Rev 2.4 + Mask Set 4N22D + 512 kBytes of P-flash + P-flash only + 128 kBytes of RAM + Board Rev ID: A + Board ID: STATIONARY + Ant ID: 12-IN G2 + Software version: 02.02.48 + CCLK: 96000000 + BCLK: 48000000 + Flash Base Address: 65536 + Flash Size: 458752 +``` + +Key details: +- **System ID:** `TWELINCH` — "Twelve Inch" (12" dish diameter) +- **Ant ID:** `12-IN G2` — Carryout G2 model identifier +- **Silicon:** K60-144pin, Rev 2.4, Mask 4N22D — NXP MK60DN512VLQ10 +- **Clocks:** CCLK=96 MHz (core), BCLK=48 MHz (bus) +- **Flash:** Base at 0x10000 (64KB), size 458752 bytes (448KB usable firmware space) +- **NVS Version:** 1.02.13 — the non-volatile storage schema version + +Note: `tasks` and `kill` commands (available on other variants like HAL 0.0.00) +are **not present** in the G2's OS submenu. On the G2, the satellite search is +disabled permanently via NVS index 20 instead of killing a running task. + +## Firmware Command Behavior Notes + +### Command Types + +- **One-shot:** Executes once, returns result, shows prompt (`>`). Safe. +- **Streaming:** Runs indefinitely until interrupted. Some accept `q` to stop, + others require closing the serial port. The `ts`, `agc`, and `lnbv` commands + are known streamers. +- **Write:** Modifies state (motor position, NVS values, fault registers). Use + with caution. + +### Serial Protocol Notes + +- Firmware expects ASCII CR (`0x0D`) as line terminator +- Response terminates with `>` prompt character (ASCII 62) +- Console does not support backspace — press Enter to clear on typo +- Streaming commands consume all serial input while running +- Some commands (e.g., `st`) are setters that return "Invalid params" when + called without arguments — they are not read-only despite appearing in + help listings without obvious setter syntax + +## TODO: Physical Board Inspection + +Tasks for when the dome housing is opened and the control PCB is accessible. + +### USB Port Investigation + +- [ ] Inspect LQFP-144 package pins 19-22 (USB0_DP, USB0_DM, VOUT33, VREGIN) + — these are on the corner near pin 1. Look for traces routing to a + connector, test pads, or unpopulated header +- [ ] Look for a USB mini/micro/Type-A connector anywhere on the PCB (may be + behind a panel, under a shield can, or on the back side) +- [ ] Check for a VBUS sense GPIO trace — the K60 needs a GPIO pin to detect + USB cable insertion (ref manual Section 3.9.2) +- [ ] If USB pads found: probe with multimeter for continuity to K60 pins 19-22 + +### Debug / Programming Interface + +- [ ] Look for SWD/JTAG debug header (K60 has dedicated SWD pins: SWDIO on + PTA3/pin 50, SWDCLK on PTA0/pin 46, SWO on PTA2/pin 49, RESET on pin 74). + Could be a 10-pin Cortex Debug connector, 2x5 shrouded header, or + unpopulated pads +- [ ] Check for a UART boot mode pin — K60 supports serial bootloader via + UART if NMI/FOPT bits are configured. Look for jumpers or DIP switches + near the MCU +- [ ] Identify the boot flash at 0x00000-0x0FFFF — is it internal to the K60 + or an external SPI flash? Check for small SOIC-8 flash chips near MCU + +### Firmware Extraction via SWD + +Serial bootloader has no interactive mode (confirmed: no interrupt window, +no baud rate trick, binary-only status bytes `01 00` and `98 80 96`). SWD +is the primary extraction path. + +**Equipment:** SWD probe (ST-Link V2, J-Link, or similar) + 4 jumper wires. + +**Connections (minimum 3 wires + GND):** + +| Signal | K60 Pin | LQFP-144 | Notes | +|--------|---------|----------|-------| +| SWDIO | PTA3 | Pin 50 | Bidirectional data | +| SWDCLK | PTA0 | Pin 46 | Clock (probe drives) | +| GND | — | Multiple | Common ground with probe | +| RESET | — | Pin 74 | Optional but recommended | +| SWO | PTA2 | Pin 49 | Optional trace output | + +**Step 1: Check flash security (CRITICAL — do this first)** + +```bash +# pyocd (recommended) +pyocd cmd -t mk60dn512xxx10 -c "read32 0x40C" + +# or openocd +openocd -f interface/stlink.cfg -f target/k60.cfg \ + -c "init; halt; mdw 0x40C; exit" +``` + +The Flash Security register (FTFL_FSEC) at `0x40C`: +- Bits [1:0] = `10` → **SECURED** — SWD reads blocked, need EzPort fallback +- Bits [1:0] = `00` or `11` → **UNSECURED** — full flash dump possible + +Also check `0x400` (FTFL_FOPT / Flash Configuration Field at 0x400-0x40F): +```bash +pyocd cmd -t mk60dn512xxx10 -c "read8 0x400 16" +``` +This 16-byte field (programmed into flash at 0x400) controls boot security, +backdoor key, mass erase enable, and other flash protection settings. + +**Step 2: Dump firmware (if unsecured)** + +```bash +# Full 512KB flash dump (bootloader + application) +pyocd flash -t mk60dn512xxx10 -r firmware_full.bin \ + --address 0x00000 --size 0x80000 + +# Or just the application (448KB) +pyocd flash -t mk60dn512xxx10 -r firmware_app.bin \ + --address 0x10000 --size 0x70000 + +# Also dump the 128KB RAM (may contain runtime state) +pyocd cmd -t mk60dn512xxx10 \ + -c "savemem 0x1FFF0000 0x20020000 sram_dump.bin" +``` + +**Step 3: If flash is secured — EzPort fallback** + +EzPort is an SPI-based flash interface activated by holding PTA4 (pin 51) +low during reset. It may bypass flash security for reads on some K60 silicon +revisions (Rev 2.4 / mask 4N22D — check errata). + +``` +EzPort SPI wiring: + EZP_CS = PTA4 (pin 51) — hold LOW during reset to enter EzPort + EZP_CLK = PTA0 (pin 46) — same pin as SWDCLK! + EZP_DOUT = PTA2 (pin 49) — same pin as SWO + EZP_DIN = PTA1 (pin 47) +``` + +EzPort commands: `0x03` = Read, `0x05` = Read Status, `0x0B` = Fast Read. +Use an SPI master (ESP32, RPi, FTDI MPSSE) at ≤ 4 MHz. + +**Flash layout (from `os id`):** + +``` +0x00000 - 0x0FFFF Bootloader v1.01 (64KB) +0x10000 - 0x7FFFF Application v02.02.48 (448KB) + Total: 512KB (0x80000) +``` + +**Boot sequence bytes (from serial capture):** + +``` +reboot echo → 0x01 0x00 (binary status) → "Bootloader version: 1.01" +→ "Application is running..." → 0x98 0x80 0x96 (binary, meaning unknown) +→ "Application Starting Kinetis PCB..." +``` + +The `0x01 0x00` and `0x98 0x80 0x96` are binary protocol bytes between +bootloader and application — possibly a handshake or integrity check. +Understanding these requires disassembling the bootloader. + +### Component Identification + +- [ ] Photograph both sides of the PCB (high-res, good lighting) +- [ ] Read markings on the BCM4515 DVB tuner IC — confirm package, check for + additional Broadcom support ICs nearby +- [ ] Identify the two A3981 stepper driver ICs (TSSOP-28 with exposed pad, + should be near motor connectors). Note their orientation and surrounding + passives (sense resistors for current measurement) +- [ ] Read crystal oscillator marking — frequency determines PLL configuration + (K60 accepts 3-32 MHz, likely 8 MHz or 16 MHz for clean 96 MHz PLL) +- [ ] Check for external EEPROM (separate from K60 internal flash) — likely + I2C EEPROM near MCU, possibly AT24Cxx or M24Cxx series +- [ ] Look for LNB voltage regulator circuit (13V/18V switching) — probably + near the coax F-connector + +### Gyro / IMU Investigation + +- [ ] NVS indices 72-77 configure gyro parameters (sensitivity, filter, mount + type) but boot log says nothing about a gyro. Check if there's an + onboard MEMS gyro or an unpopulated footprint for one +- [ ] If present, identify the gyro IC (likely single-axis rate gyro given + the NVS parameters — "Gyro Mount Type: 1" suggests a specific axis) +- [ ] If unpopulated: the G2 may share a PCB design with a larger model + (Trav'ler SK-1000?) that includes a gyro for in-motion tracking + +### GPS Investigation + +- [ ] Boot log says "GPS Not Found" — look for a GPS module footprint + (populated or empty). NVS 63-64 configure GPS heading/moving thresholds +- [ ] Check for a GPS antenna connector (SMA or U.FL) or ceramic patch antenna + on the PCB +- [ ] If unpopulated: same shared-PCB theory as the gyro — the G2 firmware + has GPS support but the hardware may be DNP (Do Not Populate) + +### Motor / Drive Train + +- [ ] Trace SPI bus from K60 to A3981 drivers — identify which DSPI peripheral + (DSPI0, DSPI1, or DSPI2) connects to which axis (AZ vs EL) +- [ ] Identify motor sense resistors near A3981 chips — value determines + current limit calibration. Compare with NVS current limit hex values + (indices 95-98, 110-111) +- [ ] Check motor connectors — are they direct stepper connections (4-wire) or + do they go through additional driver stages? +- [ ] Look for limit switches or optical encoders (the G2 uses stall detection + for homing, but there may be unused provisions for position feedback) + +### RF Signal Path + +- [ ] Trace signal path: F-connector → LNB bias tee → BCM4515 RF input +- [ ] Identify any bandpass filters, LNAs, or frequency conversion stages + between the coax and the BCM4515 +- [ ] Check if the ADC RSSI signal is a dedicated analog output from BCM4515 + or if it's derived from an AGC control voltage + +### Power Supply + +- [ ] Identify main voltage rails — the board likely has 12V input (from + RP-SK87 PSU), 5V (for logic), 3.3V (K60 core), and motor supply +- [ ] Check if the USB VREG (K60 pins 21-22) is connected to anything or + if VOUT33 is tied to the board's 3.3V rail directly +- [ ] Identify motor power supply — A3981 supports up to 28V, the board + likely runs motors at 12V from the main supply + +## TODO: Firmware Exploration (Serial Console) + +Remaining commands to test via the RS-422 console. + +### DVB Submenu (Task #13) — COMPLETE + +- [x] `ls` — streaming transponder scan (exits DVB submenu on interrupt!) +- [x] `qls` — streaming quick lock status (~100ms: `Lock:0 rssi:500 cnt:0`) +- [x] `snr` — streaming SNR readings (transient spikes in noise floor) +- [x] `diag` — multi-block one-shot per-transponder diagnostics +- [x] `table` — full 32-transponder scan (~136s), detailed per-xp data +- [x] `freqs` — frequency list name ("Non-Stacked") +- [x] `stats` — streaming, silent when no lock +- [x] `nid` — streaming network ID (FFFF/none), CR-overwrite display +- [x] `di2id` / `di2stat` / `di2conf` / `di2sc` / `di2rcs` — all fail (no switch) +- [x] `di2cs` — needs parameters (switch config) +- [x] `send` — needs 3-6 hex bytes (raw DiSEqC packet) +- [x] `h 1-13` — full parameter help documented +- [x] `tabto` / `to` — timeout configs (table vs single tune) +- [x] `ovraddr` / `rrto` / `tdthresh` / `pretx` — DiSEqC timing params +- [x] `srch` / `shuf` / `tablex` — toggle commands +- [x] `pwr` — power state, `srch_mode` — search mode, `range` — scan range +- [x] `def` — silent defaults reset (dangerous!), `msw` — format unknown +- [ ] `e ` — edit channel params (not tested — would modify state) +- [ ] `send ` — raw DiSEqC (not tested — no switch connected) + +### MOT Submenu (Task #14) — COMPLETE + +- [x] Full `?` help listing — 25 commands discovered (see Motor Control section) +- [x] `a` supports relative moves with `+`/`-` prefix (undocumented upstream!) +- [x] `h *` homes both motors simultaneously +- [x] `l` — lists 2 motors: 0=AZIMUTH (local), 1=ELEVATION (local) +- [x] `ma` / `mv` — read motor dynamics (400 deg/s², 65/45 deg/s AZ/EL) +- [x] `p` — read step positions (19998 AZ, 3116 EL) +- [x] `elminmaxhome` — EL min=18°, max=65°, home=65° +- [x] `ela2s` / `els2a` — angle↔step conversion (linear mapping confirmed) +- [x] `e` / `r` — engage/release motors +- [x] `azscan` / `azscanwxp` — sky scan commands documented +- [x] `sd` — stall detection documented +- [x] `life` / `motorlife` / `motorboth` — factory life test commands +- [x] `pid`, `sp`, `sw`, `w`, `v`, `vms` — write-only commands documented +- [ ] `g ` — NOT available on G2 (not in help listing) +- [ ] Test `h 0` and `h 1` homing (requires clear space around dish) +- [ ] Test `azscanwxp` with real scan parameters (RF imaging) + +### NVS Experiments (Caution) + +- [ ] NVS 2 ("Debug 2nd Console Port") — try setting to 1, check if USB + console activates. **Save original value first, restore after test** +- [ ] NVS 4 ("Debug Port Connection") — similar test +- [ ] NVS 112 ("Disable Dipswitch?") — what happens if dipswitch is disabled? +- [ ] NVS 38 ("Sleep Mode Timer") — currently 420s (7 min). Set higher to + prevent sleep during long tracking sessions + +### A3981 Submenu (Task #16) — COMPLETE + +- [x] Full `?` help listing — 6 commands: `diag`, `sm`, `ss`, `st`, `cm`, `reset` +- [x] `diag` — AZ/EL both OK (no faults) +- [x] `sm` — step size mode: both AUTO +- [x] `ss` — step size: both 1 (1/16 microstepping, finest) +- [x] `st` — torque: both LOW (idle) +- [x] `cm` — current control mode: both AUTO +- [ ] Test `diag` output under motor load (move motor, read diag simultaneously) +- [ ] Test changing step mode from AUTO to manual +- [ ] `reset` — clear fault flags (no faults to clear currently) + +### ADC Submenu (Task #16) — COMPLETE + +- [x] Full `?` help listing — 5 commands: `bdid`, `bdrevid`, `rssi`, `m`, `scan` +- [x] `bdid` → STATIONARY, `bdrevid` → A +- [x] `rssi` → 232 (noise floor baseline) +- [x] `scan` — streaming: position + RSSI + Lock + SNR + delta per sample +- [x] `m` — returns immediately (toggle for background monitoring?) + +### Dipswitch Submenu (Task #16) — COMPLETE + +- [x] Full `?` help listing — 1 command: `dipswitch` +- [x] `dipswitch` → val:ffffff01, app_dipswitch:101 + +### GPIO Submenu (Task #16) — COMPLETE + +- [x] Full `?` help listing — 4 commands: `regs`, `r`, `w`, `dir` +- [x] `regs` — full 101-pin dump (5 ports, all pin states) +- [x] `r ` / `w ` / `dir ` — help documented +- [ ] Map specific GPIO pins to hardware functions (SPI→A3981, UART, I2C, etc.) + +### LATLON Submenu (Task #16) — COMPLETE + +- [x] Full `?` help listing — 1 command: `l ` +- [x] Tested with satellite positions — self-localization algorithm confirmed + +### OS Submenu (Task #16) — COMPLETE + +- [x] Full `?` help listing — 2 commands: `id`, `reboot` (no `tasks`/`kill` on G2!) +- [x] `id` — full system identification including NVS version, MCU details, clock speeds +- [x] Deep probe: `go`/`date`/`time` initially appeared as hidden commands but were + confirmed as **false positives** — boot sequence output captured after `reboot` + caused a system restart mid-probe. All three return "Invalid command" post-boot. + +### PEAK Submenu (Task #16) — COMPLETE + +- [x] Full `?` help listing — 6 commands: `ts`, `pw`, `psnr`, `pxy1`, `stb`, `rssits` +- [x] `stb` — LNB polarity switching test (18V=504 RSSI, 13V=232 RSSI) +- [x] `rssits` — streaming every 1 min, exits submenu on interrupt +- [x] `pw` / `psnr` help — both take ` ` (signal peaking algorithms) +- [x] `pxy1` help — takes `` repeat count (2D cross-pattern peak search) +- [ ] Test `pw` / `psnr` / `pxy1` with a real satellite signal + +### EEPROM Submenu (Task #15) — COMPLETE + +- [x] Full `?` help listing — 3 commands: `ee`, `inv`, `def` +- [x] Complete EEPROM dump (all 17 indices, 0-16) via `scripts/ee_dump.py` +- [x] Bounds discovery: firmware enforces Min:0 Max:16 +- [x] Sentinel value: 0x00010101 for invalid/uninitialized entries +- [x] Only 6 indices (0-5) ever written; 6-16 factory default (invalid) +- [ ] `def` — restore factory defaults (not tested — would reset all values) +- [ ] Determine meaning of valid EEPROM values (indices 1-5) +- [ ] Restore index 0 via `def` or `ee 0 ` if original value is discovered + +### STEP Submenu (Task #16) — COMPLETE + +- [x] `e` — engage motors ("Motors engaged"), verified via MOT +- [x] `r` — release motors (documented via MOT, not tested to avoid losing position) +- [x] `p` — read step positions (AZ=19998, EL=3116) +- [x] `ma` / `mv` — read accel and velocity in raw microstep units +- [x] `pid` — **readable in STEP** (write-only in MOT): Kp=250, Kv=50 +- [x] MOT↔STEP conversion table verified (linear mapping) +- [ ] `p ` — test absolute microstep move (40000 steps/rev AZ, 24960 EL) +- [ ] `v ` — test continuous velocity mode + +### Hidden Command Deep Probe — COMPLETE + +Systematic brute-force probe of 415 candidate commands across all 13 menu levels +(root + 12 submenus). Script: `scripts/hidden_menu_probe.py --deep` + +**Results by submenu:** + +| Submenu | Probed | Hits | Known | New | Notes | +|---------|--------|------|-------|-----|-------| +| TRK> | 415 | 3 | 3 | 0 | Only q, Q, help | +| OS> | 415 | 7 | 4 | 0 | `go`/`date`/`time` were false positives (see below) | +| MOT> | 415 | 22 | 22 | 0 | All in help listing already | +| DVB> | 415 | 12 | 12 | 0 | All previously discovered | +| STEP> | 415 | 12 | 12 | 0 | Mirrors MOT (engage/release/pos/vel) | +| A3981> | 415 | 5 | 5 | 0 | All in help listing already | +| ADC> | 415 | 7 | 7 | 0 | | +| GPIO> | 415 | 7 | 7 | 0 | | +| PEAK> | 415 | 3 | 3 | 0 | | +| NVS> | 415 | 412 | n/a | 0 | False positives: auto-advance cursor | +| EE> | 415 | 3 | 3 | 0 | | +| LATLON> | 415 | 5 | 5 | 0 | | +| DIPSWITCH> | 415 | 3 | 3 | 0 | | + +**OS false positives explained:** The probe hit `reboot` early in the OS +sequence, causing a full system restart (~10s). The probe script continued +sending commands into the boot stream. `go`, `date`, and `time` appeared as +"hits" because their responses contained non-error text — but this text was +**unsolicited boot output**, not command responses: + +- `go` "response" = SPI2 initialization output (BCM4515 firmware transfer at boot) +- `date` "response" = BCM4515 hardware init (AP RAM FW VERIFIED, chip IDs) +- `time` "response" = DVB channel init (dvbPrvChangeChannelInit Complete) + +All three return "Invalid command" when tested manually in the OS submenu after +boot completes. The boot output is still valuable — it reveals the SPI2 clock +(6,857,142 Hz = 48 MHz / 7), SPI mode 3 (CPOL=1, CPHA=1), and the full +BCM4515 bring-up sequence. + +**NVS false positives:** The NVS submenu returned 412 hits because its parser +treats any unrecognized input as "read next entry" — an internal cursor +auto-advances through the NVS table. These are NOT hidden commands, just the +fallthrough behavior of the NVS `e` (edit/read) command parser. + +**Conclusion:** Zero genuinely hidden commands found across all 13 menu levels. +The G2 firmware shell is well-scoped — no memory access, no flash read/dump, +no hidden debug backdoors. Firmware extraction requires physical access +(SWD or EzPort). diff --git a/scripts/hidden_menu_probe.py b/scripts/hidden_menu_probe.py new file mode 100644 index 0000000..ea796b2 --- /dev/null +++ b/scripts/hidden_menu_probe.py @@ -0,0 +1,1043 @@ +#!/usr/bin/env python3 +"""Probe for undocumented/hidden commands on any embedded console. + +Auto-discovers the device's prompt character, error string, and submenu +structure, then sends candidate command names and flags anything that +produces a non-error response. + +Works with any prompt-based firmware console: Winegard Trav'ler / G2, +U-Boot, FreeRTOS shells, vendor debug consoles, etc. + +Usage: + uv run scripts/hidden_menu_probe.py --port /dev/ttyUSB2 --baud 115200 + uv run scripts/hidden_menu_probe.py --deep + uv run scripts/hidden_menu_probe.py --submenu mot + uv run scripts/hidden_menu_probe.py --wordlist scripts/wordlists/winegard.txt + uv run scripts/hidden_menu_probe.py --prompt "U-Boot>" --error "Unknown command" +""" + +from __future__ import annotations + +import argparse +import json +import re +import string +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path + +import serial # pyright: ignore[reportMissingImports] + +# --------------------------------------------------------------------------- +# Device profile — populated by auto-discovery or CLI overrides +# --------------------------------------------------------------------------- + + +@dataclass +class DeviceProfile: + """Everything we know (or detected) about the attached console.""" + + port: str = "/dev/ttyUSB0" + baud: int = 115200 + root_prompt: str = "" # e.g. "TRK>" + prompts: list[str] = field(default_factory=list) # all known prompts + error_string: str = "" # e.g. "Invalid command." + known_commands: set[str] = field(default_factory=set) # from help output + submenus: list[str] = field(default_factory=list) # detected submenu names + exit_cmd: str = "q" + line_ending: str = "\r" + + +# --------------------------------------------------------------------------- +# Serial I/O +# --------------------------------------------------------------------------- + + +def send_cmd( + ser: serial.Serial, + cmd: str, + profile: DeviceProfile, + timeout: float = 1.0, +) -> str: + """Send *cmd* + line-ending, read until a known prompt or timeout.""" + ser.reset_input_buffer() + ser.write(f"{cmd}{profile.line_ending}".encode("ascii", errors="replace")) + ser.timeout = timeout + + buf = bytearray() + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + chunk = ser.read(4096) + if chunk: + buf.extend(chunk) + text = buf.decode("utf-8", errors="replace") + if text.rstrip().endswith(">"): + break + elif buf: + text = buf.decode("utf-8", errors="replace") + if text.rstrip().endswith(">"): + break + + return buf.decode("utf-8", errors="replace") + + +# --------------------------------------------------------------------------- +# Auto-discovery +# --------------------------------------------------------------------------- + +_PROMPT_RE = re.compile(r"(\S+[>$#])\s*$") + + +def detect_prompt(ser: serial.Serial, profile: DeviceProfile) -> str | None: + """Send a bare line-ending and extract the prompt from the response.""" + ser.reset_input_buffer() + ser.write(profile.line_ending.encode("ascii")) + ser.timeout = 2.0 + + buf = bytearray() + deadline = time.monotonic() + 2.0 + while time.monotonic() < deadline: + chunk = ser.read(4096) + if chunk: + buf.extend(chunk) + text = buf.decode("utf-8", errors="replace") + tail = text.rstrip() + if tail.endswith((">", "#", "$")): + break + elif buf: + break + + text = buf.decode("utf-8", errors="replace").strip() + if not text: + return None + + # Take the last line, look for a prompt-like token + last_line = text.split("\n")[-1].strip() + m = _PROMPT_RE.search(last_line) + return m.group(1) if m else (last_line if last_line else None) + + +def detect_error_string(ser: serial.Serial, profile: DeviceProfile) -> str | None: + """Send a garbage command and extract the error message template.""" + resp = send_cmd(ser, "__xyzzy_probe__", profile, timeout=1.0) + + # Strip the echo of our command and any prompt + lines = resp.replace("__xyzzy_probe__", "").strip().split("\n") + # Filter out lines that are just prompts + content_lines = [] + for line in lines: + stripped = line.strip() + if not stripped: + continue + # Skip lines that are purely a prompt token + if _PROMPT_RE.fullmatch(stripped): + continue + # Strip trailing prompt from content lines + stripped = _PROMPT_RE.sub("", stripped).strip() + if stripped: + content_lines.append(stripped) + + if content_lines: + # The error message is typically a single line + return content_lines[0].strip() + return None + + +def parse_help_output( + help_text: str, + profile: DeviceProfile, +) -> tuple[set[str], list[str]]: + """Parse help output for command names and submenu hints. + + Returns (known_commands, submenu_names). + """ + commands: set[str] = set() + submenus: list[str] = [] + + # Common help-output patterns: + # "command - description" + # "command Description text" + # " command" + # "Enter - Description Menu" (Winegard G2 style) + # "Enter name sub-menu" + cmd_dash_re = re.compile(r"^\s*(\w[\w.]*)\s+[-—]\s+") + cmd_spaces_re = re.compile(r"^\s{0,4}(\w[\w.]*)\s{2,}") + bare_cmd_re = re.compile(r"^\s{0,4}(\w[\w.]*)\s*$") + # Angle-bracket format: "Enter - Description" + bracket_re = re.compile(r"<(\w[\w.]*)>") + enter_re = re.compile(r"[Ee]nter\s+?", re.IGNORECASE) + menu_re = re.compile(r"(\w+)\s+[Mm]enu") + submenu_re = re.compile(r"(\w+)\s+[Ss]ub-?[Mm]enu") + + for line in help_text.split("\n"): + stripped = line.strip() + if not stripped: + continue + # Skip lines that are just prompts + if _PROMPT_RE.fullmatch(stripped): + continue + + # Try angle-bracket commands first (e.g. "Enter - ...") + bracket_match = bracket_re.search(stripped) + if bracket_match: + cmd_name = bracket_match.group(1).lower() + if len(cmd_name) <= 20: + commands.add(cmd_name) + else: + # Fall back to generic patterns + for pat in (cmd_dash_re, cmd_spaces_re, bare_cmd_re): + m = pat.match(stripped) + if m: + cmd_name = m.group(1).lower() + if len(cmd_name) <= 20: + commands.add(cmd_name) + break + + # Look for submenu hints — prefer bracketed names + enter_match = enter_re.search(stripped) + if enter_match: + sub = enter_match.group(1).lower() + if sub not in submenus and len(sub) <= 20: + submenus.append(sub) + else: + for pat in (submenu_re, menu_re): + m = pat.search(stripped) + if m: + sub = m.group(1).lower() + if sub not in submenus and len(sub) <= 20: + submenus.append(sub) + break + + # Any command that appears in the help output might also be a submenu + # if the help text mentions "Enter " or " Menu". + # Commands themselves that are single-word and also in submenus get both. + + return commands, submenus + + +def auto_discover(ser: serial.Serial, profile: DeviceProfile) -> DeviceProfile: + """Run the discovery sequence and populate the profile.""" + # Step 1: Detect root prompt + print("Phase 1: Auto-discovering device console...\n") + + prompt = detect_prompt(ser, profile) + if prompt: + profile.root_prompt = prompt + profile.prompts = [prompt] + print(f" Root prompt: {prompt}") + else: + print(" WARNING: Could not detect root prompt.") + print(" Use --prompt to specify manually.") + + # Step 2: Detect error string + err = detect_error_string(ser, profile) + if err: + profile.error_string = err + print(f' Error string: "{err}"') + else: + print(" WARNING: Could not detect error string.") + print(" Use --error to specify manually.") + + # Step 3: Parse help output + print(" Sending help command: ?") + help_resp = send_cmd(ser, "?", profile, timeout=2.0) + + commands, submenus = parse_help_output(help_resp, profile) + if commands: + profile.known_commands = commands + print(f" Known commands ({len(commands)}): {', '.join(sorted(commands))}") + else: + print(" No commands parsed from help output.") + + if submenus: + profile.submenus = submenus + print(f" Detected submenus ({len(submenus)}): {', '.join(submenus)}") + else: + # Fall back: any known command that looks like a submenu name + # (we'll discover actual submenus during probing) + print(" No submenus detected from help output.") + + # Build prompt list from submenus + for sub in profile.submenus: + sub_prompt = f"{sub.upper()}>" + if sub_prompt not in profile.prompts: + profile.prompts.append(sub_prompt) + + print() + return profile + + +# --------------------------------------------------------------------------- +# Navigation +# --------------------------------------------------------------------------- + + +def navigate_to_root(ser: serial.Serial, profile: DeviceProfile) -> str: + """Send exit command until we're at the root prompt. + + Sends a bare line-ending first to check if we're already at root, + avoiding the case where 'q' at root level kills the shell entirely. + """ + # Check if we're already at root (bare CR won't kill anything) + resp = send_cmd(ser, "", profile) + if profile.root_prompt and profile.root_prompt in resp: + return profile.root_prompt + + for _ in range(5): + resp = send_cmd(ser, profile.exit_cmd, profile) + if profile.root_prompt and profile.root_prompt in resp: + return profile.root_prompt + # If no root prompt set, check if the last line looks prompt-like + last_line = resp.strip().split("\n")[-1].strip() + m = _PROMPT_RE.search(last_line) + if m: + detected = m.group(1) + if not profile.root_prompt: + profile.root_prompt = detected + if detected == profile.root_prompt: + return detected + + # Last resort: bare line-ending + resp = send_cmd(ser, "", profile) + last_line = resp.strip().split("\n")[-1].strip() + return last_line + + +def enter_submenu(ser: serial.Serial, menu: str, profile: DeviceProfile) -> str: + """Enter a submenu and return the prompt we land on.""" + navigate_to_root(ser, profile) + resp = send_cmd(ser, menu, profile) + lines = resp.strip().split("\n") + last = lines[-1].strip() if lines else "" + + # Register this prompt if new + m = _PROMPT_RE.search(last) + if m: + new_prompt = m.group(1) + if new_prompt not in profile.prompts: + profile.prompts.append(new_prompt) + return new_prompt + return last + + +# --------------------------------------------------------------------------- +# Probing +# --------------------------------------------------------------------------- + + +def clean_response( + resp: str, + cmd: str, + profile: DeviceProfile, +) -> str: + """Strip echo, prompts, and whitespace from a response.""" + clean = resp + + # Remove echo of the command (with various line-ending combos) + for suffix in ("\r\n", "\r", "\n", ""): + clean = clean.replace(f"{cmd}{suffix}", "", 1) + + clean = clean.strip() + + # Remove any known prompt tokens + for p in profile.prompts: + clean = clean.replace(p, "") + + # Also strip any bare prompt-like token at the very end + clean = _PROMPT_RE.sub("", clean) + + return clean.strip() + + +def probe_commands( + ser: serial.Serial, + candidates: list[str], + prompt: str, + label: str, + profile: DeviceProfile, + timeout: float = 0.5, +) -> list[tuple[str, str]]: + """Probe candidates at the current menu level. Returns (cmd, preview) hits.""" + hits = [] + total = len(candidates) + + for i, cmd in enumerate(candidates): + if (i + 1) % 50 == 0: + print(f" [{label}] Progress: {i + 1}/{total}...", flush=True) + + resp = send_cmd(ser, cmd, profile, timeout=timeout) + clean = clean_response(resp, cmd, profile) + + # A "hit" is anything that isn't the error string and has content + is_error = profile.error_string and profile.error_string in resp + if not is_error and clean: + preview = clean[:100].replace("\r\n", " | ").replace("\n", " | ") + hits.append((cmd, preview)) + print(f" *** HIT: '{cmd}' -> {preview}", flush=True) + + # If the command kicked us out of the submenu, recover + if "Terminating shell" in resp or "exiting" in resp.lower(): + if profile.root_prompt and prompt != profile.root_prompt: + print(f" ('{cmd}' exited submenu, re-entering...)") + menu_name = prompt.replace(">", "").strip().lower() + enter_submenu(ser, menu_name, profile) + else: + # At root — shell may have died. Wait for firmware + # to respawn it, then verify with a bare CR. + print(f" ('{cmd}' terminated shell, waiting for restart...)") + time.sleep(1.0) + check = send_cmd(ser, "", profile) + if profile.root_prompt and profile.root_prompt not in check: + print( + " WARNING: Shell did not restart. Remaining " + "results may be incomplete.", + flush=True, + ) + + return hits + + +# --------------------------------------------------------------------------- +# Candidate generation +# --------------------------------------------------------------------------- + + +def generate_candidates( + blocklist: set[str], + wordlist_paths: list[Path] | None = None, +) -> list[str]: + """Build the candidate command list. + + Includes generic embedded debug commands + single chars + two-letter combos. + Merges in any external wordlist files. Applies blocklist last. + """ + candidates: list[str] = [] + + # Single characters + candidates.extend(list(string.ascii_lowercase)) + candidates.extend(list(string.ascii_uppercase)) + candidates.extend(list(string.digits)) + + # Generic embedded debug commands (no device-specific words) + generic = [ + # Memory access + "md", + "mw", + "mm", + "mr", + "mem", + "peek", + "poke", + "rd", + "wr", + "read", + "write", + "dump", + "load", + "save", + "md.b", + "md.w", + "md.l", + "x", + "xx", + "xd", + # Flash + "flash", + "fl", + "erase", + "program", + "verify", + "protect", + "flinfo", + "fldump", + "flashdump", + # Boot / system + "boot", + "reboot", + "reset", + "go", + "run", + "exec", + "jump", + "bootd", + "bootm", + "bootp", + "version", + "ver", + "info", + "about", + "sysinfo", + "uptime", + "date", + "time", + "clk", + "clock", + # Debug + "debug", + "dbg", + "trace", + "log", + "print", + "echo", + "test", + "diag", + "selftest", + "bist", + "bench", + "assert", + "crash", + "fault", + "panic", + # Shell / OS + "sh", + "shell", + "cmd", + "command", + "cli", + "task", + "tasks", + "ps", + "top", + "threads", + "kill", + "suspend", + "resume", + "heap", + "stack", + "free", + "malloc", + "meminfo", + "cpu", + "cpuinfo", + "temp", + "temperature", + # Network / comms + "ping", + "net", + "ifconfig", + "ip", + "mac", + "uart", + "serial", + "spi", + "i2c", + "can", + # Service / factory + "factory", + "service", + "mfg", + "production", + "prod", + "cal", + "calibrate", + "calibration", + "config", + "cfg", + "setup", + "settings", + "hidden", + "secret", + "admin", + "su", + "root", + "login", + "password", + "passwd", + "auth", + "unlock", + # Update + "update", + "upgrade", + "firmware", + "fw", + "ota", + "download", + "upload", + "xmodem", + "ymodem", + "zmodem", + "tftp", + "ftp", + # Generic hardware + "sw", + "hw", + "id", + "sn", + "help", + "man", + "usage", + # Two-letter combos + "bl", + "bt", + "db", + "dm", + "dp", + "ds", + "dt", + "eb", + "ed", + "ee", + "ef", + "em", + "en", + "ep", + "er", + "es", + "et", + "fa", + "fb", + "fc", + "fd", + "fe", + "ff", + "fg", + "fh", + "fi", + "fj", + "ga", + "gb", + "gc", + "gd", + "ge", + "gf", + "gg", + "gh", + "gi", + "gj", + "ha", + "hb", + "hc", + "hd", + "he", + "hf", + "hg", + "hh", + "hi", + "hj", + "ia", + "ib", + "ic", + "io", + "ir", + "ka", + "kb", + "kc", + "kd", + "ke", + "la", + "lb", + "lc", + "ld", + "le", + "lf", + "lg", + "lh", + "li", + "lj", + "ma", + "mb", + "mc", + "me", + "mf", + "mg", + "mh", + "mi", + "mj", + "na", + "nb", + "nc", + "nd", + "ne", + "nf", + "ng", + "nh", + "ni", + "nj", + "oa", + "ob", + "oc", + "od", + "oe", + "of", + "og", + "oh", + "oi", + "oj", + "pa", + "pb", + "pc", + "pd", + "pe", + "pf", + "pg", + "ph", + "pi", + "pj", + "ra", + "rb", + "rc", + "re", + "rf", + "rg", + "rh", + "ri", + "rj", + "sa", + "sb", + "sc", + "sd", + "se", + "sf", + "sg", + "si", + "sj", + "ta", + "tb", + "tc", + "td", + "te", + "tf", + "tg", + "th", + "ti", + "tj", + "ua", + "ub", + "uc", + "ud", + "ue", + "uf", + "ug", + "uh", + "ui", + "uj", + "va", + "vb", + "vc", + "vd", + "ve", + "vf", + "vg", + "vh", + "vi", + "vj", + "wa", + "wb", + "wc", + "wd", + "we", + "wf", + "wg", + "wh", + "wi", + "wj", + "za", + "zb", + "zc", + "zd", + "ze", + "zf", + ] + candidates.extend(generic) + + # Merge external wordlists + if wordlist_paths: + for wl_path in wordlist_paths: + try: + text = wl_path.read_text() + for line in text.split("\n"): + word = line.strip() + # Skip blanks and comments + if word and not word.startswith("#"): + candidates.append(word) + except OSError as exc: + print( + f"WARNING: Could not read wordlist {wl_path}: {exc}", + file=sys.stderr, + ) + + # Deduplicate preserving order + seen: set[str] = set() + unique: list[str] = [] + for c in candidates: + if c not in seen: + seen.add(c) + unique.append(c) + + # Apply blocklist + unique = [c for c in unique if c not in blocklist] + + return unique + + +# --------------------------------------------------------------------------- +# JSON output +# --------------------------------------------------------------------------- + + +def write_json_report( + path: Path, + profile: DeviceProfile, + results: dict[str, list[tuple[str, str]]], +) -> None: + """Write machine-readable JSON probe report.""" + report: dict = { + "device": {"port": profile.port, "baud": profile.baud}, + "detected": { + "root_prompt": profile.root_prompt, + "error_string": profile.error_string, + "known_commands": sorted(profile.known_commands), + "submenus": profile.submenus, + }, + "results": {}, + } + + for label, hits in results.items(): + known_hits = [ + (cmd, resp) for cmd, resp in hits if cmd.lower() in profile.known_commands + ] + unknown_hits = [ + (cmd, resp) + for cmd, resp in hits + if cmd.lower() not in profile.known_commands + ] + report["results"][label] = { + "total_hits": len(hits), + "known": len(known_hits), + "unknown": len(unknown_hits), + "hits": [{"cmd": cmd, "response": resp} for cmd, resp in hits], + } + + path.write_text(json.dumps(report, indent=2) + "\n") + print(f"\nJSON report written to {path}") + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +LINE_ENDINGS = {"cr": "\r", "lf": "\n", "crlf": "\r\n"} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Probe for hidden commands on an embedded console", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +examples: + %(prog)s --port /dev/ttyUSB2 --baud 115200 + %(prog)s --deep + %(prog)s --submenu mot + %(prog)s --wordlist scripts/wordlists/winegard.txt + %(prog)s --prompt "U-Boot>" --error "Unknown command" +""", + ) + + conn = parser.add_argument_group("connection") + conn.add_argument( + "--port", default="/dev/ttyUSB0", help="Serial port (default: /dev/ttyUSB0)" + ) + conn.add_argument( + "--baud", type=int, default=115200, help="Baud rate (default: 115200)" + ) + conn.add_argument( + "--line-ending", + choices=LINE_ENDINGS, + default="cr", + help="Line ending to send (default: cr)", + ) + + disc = parser.add_argument_group("discovery overrides") + disc.add_argument( + "--prompt", default=None, help="Override auto-detected root prompt" + ) + disc.add_argument( + "--error", default=None, help="Override auto-detected error string" + ) + disc.add_argument( + "--help-cmd", default="?", help="Command to request help (default: ?)" + ) + disc.add_argument( + "--exit-cmd", default="q", help="Command to exit submenu (default: q)" + ) + + probe = parser.add_argument_group("probing") + probe.add_argument( + "--deep", action="store_true", help="Probe all discovered submenus" + ) + probe.add_argument( + "--submenu", type=str, default=None, help="Probe a single submenu by name" + ) + probe.add_argument( + "--timeout", + type=float, + default=0.5, + help="Per-command timeout in seconds (default: 0.5)", + ) + probe.add_argument( + "--blocklist", + default="reboot,stow,def,q,Q", + help="Comma-separated commands to never send (default: reboot,stow,def,q,Q)", + ) + probe.add_argument( + "--wordlist", + action="append", + default=None, + metavar="FILE", + help="Extra candidate words file (one per line, repeatable)", + ) + + output = parser.add_argument_group("output") + output.add_argument( + "--json", metavar="FILE", default=None, help="Write results as JSON to FILE" + ) + + return parser.parse_args() + + +def main() -> None: + args = parse_args() + + # Build profile from CLI args + profile = DeviceProfile( + port=args.port, + baud=args.baud, + line_ending=LINE_ENDINGS[args.line_ending], + exit_cmd=args.exit_cmd, + ) + + # Apply explicit overrides (before auto-discovery) + if args.prompt: + profile.root_prompt = args.prompt + profile.prompts = [args.prompt] + if args.error: + profile.error_string = args.error + + # Build candidate list + blocklist = {w.strip() for w in args.blocklist.split(",") if w.strip()} + wordlist_paths = [Path(p) for p in args.wordlist] if args.wordlist else None + candidates = generate_candidates(blocklist, wordlist_paths) + print(f"Generated {len(candidates)} candidate commands\n") + + # Open serial + ser = serial.Serial(profile.port, profile.baud, timeout=1) + ser.reset_input_buffer() + + # Collect results for JSON report + all_results: dict[str, list[tuple[str, str]]] = {} + + try: + # Auto-discovery (or validate overrides) + if not profile.root_prompt or not profile.error_string: + profile = auto_discover(ser, profile) + else: + print("Using overrides:") + print(f" Root prompt: {profile.root_prompt}") + print(f' Error string: "{profile.error_string}"') + + # Still run help to discover known commands and submenus + print(f" Sending help command: {args.help_cmd}") + help_resp = send_cmd(ser, args.help_cmd, profile, timeout=2.0) + commands, submenus = parse_help_output(help_resp, profile) + profile.known_commands = commands + if not profile.submenus: + profile.submenus = submenus + for sub in profile.submenus: + sub_prompt = f"{sub.upper()}>" + if sub_prompt not in profile.prompts: + profile.prompts.append(sub_prompt) + + if commands: + print( + f" Known commands ({len(commands)}): {', '.join(sorted(commands))}" + ) + if submenus: + print(f" Submenus ({len(submenus)}): {', '.join(submenus)}") + print() + + if not profile.root_prompt: + print( + "ERROR: Could not determine root prompt. Use --prompt to specify.", + file=sys.stderr, + ) + sys.exit(1) + + # Navigate to root + prompt = navigate_to_root(ser, profile) + print(f"Starting at: {prompt}\n") + + # Probe root menu + root_label = profile.root_prompt.rstrip(">$#").strip() or "ROOT" + print( + f"=== Probing {profile.root_prompt} (root) " + f"-- {len(candidates)} candidates ===\n" + ) + + root_hits = probe_commands( + ser, + candidates, + profile.root_prompt, + root_label, + profile, + timeout=args.timeout, + ) + all_results[root_label] = root_hits + + # Classify hits + unknown_hits = [ + (cmd, resp) + for cmd, resp in root_hits + if cmd.lower() not in profile.known_commands + ] + known_count = len(root_hits) - len(unknown_hits) + + print("\n--- Root Results ---") + print(f" Total hits: {len(root_hits)}") + print(f" Known commands: {known_count}") + print(f" UNKNOWN commands: {len(unknown_hits)}") + for cmd, resp in unknown_hits: + print(f" '{cmd}' -> {resp}") + + # Determine which submenus to probe + submenus_to_probe: list[str] = [] + if args.submenu: + submenus_to_probe.append(args.submenu) + elif args.deep: + submenus_to_probe = list(profile.submenus) + + for menu in submenus_to_probe: + label = menu.upper() + print(f"\n=== Probing {label} submenu ===\n") + sub_prompt = enter_submenu(ser, menu, profile) + print(f" Prompt: {sub_prompt}") + + sub_hits = probe_commands( + ser, + candidates, + sub_prompt, + label, + profile, + timeout=args.timeout, + ) + all_results[label] = sub_hits + + print(f"\n--- {label} Results: {len(sub_hits)} hits ---") + for cmd, resp in sub_hits: + print(f" '{cmd}' -> {resp}") + + navigate_to_root(ser, profile) + + # JSON report + if args.json: + write_json_report(Path(args.json), profile, all_results) + + except KeyboardInterrupt: + print("\n\nInterrupted.") + finally: + ser.close() + print("\nPort closed.") + + +if __name__ == "__main__": + main() diff --git a/scripts/wordlists/winegard.txt b/scripts/wordlists/winegard.txt new file mode 100644 index 0000000..1a05847 --- /dev/null +++ b/scripts/wordlists/winegard.txt @@ -0,0 +1,60 @@ +# Winegard satellite dish firmware - device-specific candidate commands +# Load with: --wordlist scripts/wordlists/winegard.txt + +# Dish / antenna +stow +deploy +park +home +search +scan +find +locate +track +point +dish +antenna +ant +feed + +# Satellite / signal +sat +satellite +lnb +pol +polarity +rf +signal +snr +ber +rssi +blind +ngsearch + +# Units +idu +odu +iru + +# GPS / position +gps +nmea +position +pos +loc +gyro +imu +accel +tilt +level + +# Motor / motion +motor +drive +move +goto +slew +az +el +sk +skew