Initial commit: Birdcage documentation site
Starlight/Astro docs covering hardware reverse engineering, satellite tracking guides, firmware command reference, and engineering journal entries from the Carryout G2 exploration. 32 pages across getting-started, guides, reference, understanding, and journal sections.
This commit is contained in:
commit
088c1a5ace
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
45
astro.config.mjs
Normal file
45
astro.config.mjs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import starlight from '@astrojs/starlight';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
telemetry: false,
|
||||||
|
devToolbar: { enabled: false },
|
||||||
|
integrations: [
|
||||||
|
starlight({
|
||||||
|
title: "Birdcage",
|
||||||
|
description: 'Winegard satellite dish control for amateur radio sky tracking',
|
||||||
|
social: [
|
||||||
|
{
|
||||||
|
icon: 'github',
|
||||||
|
label: 'GitHub',
|
||||||
|
href: 'https://github.com/saveitforparts/Travler_Rotor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customCss: ['./src/styles/custom.css'],
|
||||||
|
sidebar: [
|
||||||
|
{ label: 'Home', link: '/' },
|
||||||
|
{
|
||||||
|
label: 'Getting Started',
|
||||||
|
autogenerate: { directory: 'getting-started' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guides',
|
||||||
|
autogenerate: { directory: 'guides' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reference',
|
||||||
|
autogenerate: { directory: 'reference' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Understanding',
|
||||||
|
autogenerate: { directory: 'understanding' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Project Journal',
|
||||||
|
badge: { text: 'Living', variant: 'note' },
|
||||||
|
autogenerate: { directory: 'journal' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
6851
package-lock.json
generated
Normal file
6851
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "birdcage-docs",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/starlight": "^0.37.6",
|
||||||
|
"astro": "^5.17.2",
|
||||||
|
"sharp": "^0.34.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/datasheets/A3981-datasheet.pdf
Normal file
BIN
public/datasheets/A3981-datasheet.pdf
Normal file
Binary file not shown.
24349
public/datasheets/K60-datasheet.pdf
Normal file
24349
public/datasheets/K60-datasheet.pdf
Normal file
File diff suppressed because it is too large
Load Diff
431281
public/datasheets/K60-reference-manual.pdf
Normal file
431281
public/datasheets/K60-reference-manual.pdf
Normal file
File diff suppressed because one or more lines are too long
BIN
public/datasheets/RYS352A.pdf
Normal file
BIN
public/datasheets/RYS352A.pdf
Normal file
Binary file not shown.
BIN
public/datasheets/RYS352x_PAIR_Command_Guide.pdf
Normal file
BIN
public/datasheets/RYS352x_PAIR_Command_Guide.pdf
Normal file
Binary file not shown.
BIN
src/assets/carryout-g2.jpg
Normal file
BIN
src/assets/carryout-g2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
7
src/content.config.ts
Normal file
7
src/content.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineCollection } from 'astro:content';
|
||||||
|
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||||
|
import { docsSchema } from '@astrojs/starlight/schema';
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||||
|
};
|
||||||
170
src/content/docs/getting-started/first-connection.mdx
Normal file
170
src/content/docs/getting-started/first-connection.mdx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
title: Your First Connection
|
||||||
|
description: Step-by-step guide to connecting to your Winegard dish firmware console over serial.
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This tutorial walks you through physically connecting to your dish, finding the serial port on your computer, and getting the firmware prompt to respond.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Winegard dish with power supply, plugged in
|
||||||
|
- The correct serial adapter for your variant (see [What You Need](/getting-started/))
|
||||||
|
- `birdcage` and `console-probe` installed via `uv sync`
|
||||||
|
|
||||||
|
## Connect to the firmware console
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Wire up the adapter**
|
||||||
|
|
||||||
|
Connect your serial adapter to the dish's RJ-25 (Trav'ler) or RJ-12 (Carryout G2) jack. Make sure the power supply is connected but the dish does not need to be powered on yet.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Trav'ler (RS-485)">
|
||||||
|
Connect your USB-to-RS232 adapter to the DTECH RS232-to-RS485 converter. Wire the RS-485 A/B outputs to pins 2-3 of the RJ-25 cable. Pin 1 is ground.
|
||||||
|
|
||||||
|
Only two signal wires are used in half-duplex mode (plus ground).
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Carryout G2 (RS-422)">
|
||||||
|
Connect your USB-to-RS422 adapter to the RJ-12 cable. All four signal wires are used:
|
||||||
|
|
||||||
|
| Adapter Label | RJ-12 Pin | Function |
|
||||||
|
|--------------|-----------|----------|
|
||||||
|
| TX+ (TA) | Pin 2 | Computer to dish |
|
||||||
|
| TX- (TB) | Pin 3 | Computer to dish |
|
||||||
|
| RX+ (RA) | Pin 4 | Dish to computer |
|
||||||
|
| RX- (RB) | Pin 5 | Dish to computer |
|
||||||
|
| GND | Pin 1 | Ground |
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
2. **Plug in the USB adapter**
|
||||||
|
|
||||||
|
Connect the USB side of your adapter to your computer. Find the serial port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
ls /dev/tty.usbserial* /dev/cu.usbserial* 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see something like `/dev/ttyUSB0` or `/dev/ttyUSB2`. If nothing appears, check that your adapter is recognized by the OS (`dmesg | tail` on Linux).
|
||||||
|
|
||||||
|
3. **Power on the dish**
|
||||||
|
|
||||||
|
Apply power to the dish. If this is a fresh power-on, the firmware will run through its boot sequence.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Trav'ler (HAL variants)">
|
||||||
|
The Trav'ler boots and runs a calibration sequence, homing the motors to their reference positions. This takes 10-15 seconds. You'll see boot messages ending with one of:
|
||||||
|
|
||||||
|
```
|
||||||
|
NoGPS
|
||||||
|
No LNB Voltage
|
||||||
|
```
|
||||||
|
|
||||||
|
The prompt character is `>`.
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Carryout G2">
|
||||||
|
The G2 has a more verbose boot sequence:
|
||||||
|
|
||||||
|
```
|
||||||
|
Bootloader v1.01
|
||||||
|
SPI1 init @ 4 MHz (A3981 motor drivers)
|
||||||
|
Motor init (System=12Inch, master=40000 steps)
|
||||||
|
SPI2 init @ 6.857 MHz (BCM4515 DVB tuner)
|
||||||
|
EXTENDED_DVB_DEBUG ENABLED
|
||||||
|
DVB init (BCM4515 ID 0x4515 Rev B0, FW v113.37)
|
||||||
|
Enabled LNB STB
|
||||||
|
Ant ID - 12-IN G2
|
||||||
|
```
|
||||||
|
|
||||||
|
After homing (if tracker is enabled), you'll see the `TRK>` prompt. If the tracker is already disabled via NVS 20, homing is skipped and you get `TRK>` immediately.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
4. **Verify communication**
|
||||||
|
|
||||||
|
Open a quick connection to confirm the firmware responds:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Trav'ler (HAL variants)">
|
||||||
|
```bash
|
||||||
|
uv run birdcage pos --port /dev/ttyUSB0 --firmware hal205
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
AZ: 180.0
|
||||||
|
EL: 45.0
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Carryout G2">
|
||||||
|
```bash
|
||||||
|
uv run birdcage pos --port /dev/ttyUSB2 --firmware g2
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
AZ: 180.0
|
||||||
|
EL: 45.0
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
If you get a position readout, your connection is working.
|
||||||
|
|
||||||
|
5. **Explore the help menu**
|
||||||
|
|
||||||
|
To see what the firmware offers, you can use `console-probe` in discover-only mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run console-probe --port /dev/ttyUSB2 --baud 115200 --discover-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if you have a terminal emulator (like `minicom` or `screen`), connect directly and type `?` followed by Enter:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> ?
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints the list of available commands and submenus. On the Carryout G2, you'll see entries for `mot` (motor control), `dvb` (DVB tuner), `nvs` (non-volatile storage), `gpio`, and more.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="tip" title="Garbled data at the right baud rate?">
|
||||||
|
If you see garbage characters but your baud rate is correct, the RX polarity is likely swapped. On RS-422 (Carryout G2), swap the `+` and `-` wires on the **RX pair** (pins 4 and 5). Systematic bit inversion from reversed polarity produces garbled output, not random noise.
|
||||||
|
|
||||||
|
If the dish doesn't respond at all (no garbled data, just silence), the TX pair polarity may be swapped. The dish can't decode inverted framing, so it silently ignores your commands.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
<Aside type="caution" title="Do not type 'q' at the root prompt">
|
||||||
|
The `q` command at the root `TRK>` prompt terminates the firmware shell entirely. The console becomes unresponsive until you power-cycle the dish. This is not the same as exiting a submenu -- `q` inside a submenu returns you to the parent menu safely.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## What you should see
|
||||||
|
|
||||||
|
After a successful connection, you have a working serial link to the firmware console. The dish is listening for commands, and you can query its position, explore submenus, and issue motor commands.
|
||||||
|
|
||||||
|
From here, there are two paths:
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Your First Satellite Track"
|
||||||
|
href="/getting-started/first-track/"
|
||||||
|
description="Disable the TV search, start the rotctld server, and track a satellite with Gpredict."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Wiring Guide"
|
||||||
|
href="/guides/wiring/"
|
||||||
|
description="Detailed pinouts, adapter options, and cable diagrams for all variants."
|
||||||
|
/>
|
||||||
201
src/content/docs/getting-started/first-track.mdx
Normal file
201
src/content/docs/getting-started/first-track.mdx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
title: Your First Satellite Track
|
||||||
|
description: End-to-end tutorial from a connected dish to tracking a satellite with Gpredict.
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
You have a working serial connection and the dish responds to position queries. Now let's point it at something in the sky.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Serial connection confirmed working (see [Your First Connection](/getting-started/first-connection/))
|
||||||
|
- [Gpredict](https://oz9aec.net/radios/digital-modes/gpredict-real-time-satellite-tracking-and-orbit-prediction-application) installed (or any Hamlib rotctld client)
|
||||||
|
- Dish physically mounted with "BACK" label aligned to true North
|
||||||
|
- Clear sky view (no obstructions taller than 8 inches within 32.5 inches of the base center)
|
||||||
|
|
||||||
|
## Track a satellite
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Disable the TV satellite search**
|
||||||
|
|
||||||
|
On power-up, the firmware automatically starts searching for DirecTV/DISH Network satellites. This will fight your tracking commands if not stopped.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Trav'ler (HAL 2.05)">
|
||||||
|
The `birdcage init` command handles this automatically. It waits for boot to complete, then sends:
|
||||||
|
|
||||||
|
```
|
||||||
|
ngsearch → s → q
|
||||||
|
```
|
||||||
|
|
||||||
|
This enters the search submenu, stops the search, and exits. You need to run init on every power cycle.
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Trav'ler (HAL 0.0.00)">
|
||||||
|
Also handled by `birdcage init`. The older firmware uses a different approach -- killing the search task through the OS task manager:
|
||||||
|
|
||||||
|
```
|
||||||
|
os → kill Search → q
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Carryout G2">
|
||||||
|
The G2 uses a **permanent** one-time configuration. Set NVS index 20 to TRUE using a serial terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> nvs
|
||||||
|
NVS> e 20 1
|
||||||
|
NVS> s
|
||||||
|
NVS> q
|
||||||
|
```
|
||||||
|
|
||||||
|
This disables the tracker process permanently (survives power cycles). After setting this, `birdcage init` skips the search kill entirely.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Aside type="caution" title="Search mode interference">
|
||||||
|
If the TV satellite search is still running, the firmware will override your motor commands and slew the dish to its own targets. Always disable the search before tracking. On the Trav'ler variants, you must re-disable it after every power cycle.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
2. **Initialize the dish**
|
||||||
|
|
||||||
|
Run the init command to boot the dish and prepare it for motor commands:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Trav'ler">
|
||||||
|
```bash
|
||||||
|
uv run birdcage init --port /dev/ttyUSB0 --firmware hal205
|
||||||
|
```
|
||||||
|
|
||||||
|
This waits for the boot signal (`NoGPS` or `No LNB Voltage`), kills the satellite search, and enters the motor menu. It may take 30-60 seconds on first power-up while the dish homes its motors.
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Carryout G2">
|
||||||
|
```bash
|
||||||
|
uv run birdcage init --port /dev/ttyUSB2 --firmware g2
|
||||||
|
```
|
||||||
|
|
||||||
|
With the tracker already disabled via NVS 20, this connects and enters the motor menu immediately.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
```
|
||||||
|
Connecting to /dev/ttyUSB2 (firmware: g2)...
|
||||||
|
Antenna initialized and ready.
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the rotctld server**
|
||||||
|
|
||||||
|
The `serve` command starts a TCP server that speaks the Hamlib rotctld protocol:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run birdcage serve \
|
||||||
|
--port /dev/ttyUSB2 \
|
||||||
|
--firmware g2 \
|
||||||
|
--host 127.0.0.1 \
|
||||||
|
--listen-port 4533 \
|
||||||
|
--skip-init
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--skip-init` if you already ran `init` separately. Without it, the serve command runs the full initialization sequence first.
|
||||||
|
|
||||||
|
```
|
||||||
|
rotctld server listening on 127.0.0.1:4533
|
||||||
|
Ctrl-C to stop
|
||||||
|
```
|
||||||
|
|
||||||
|
The server handles these commands from Gpredict:
|
||||||
|
|
||||||
|
| Command | Function |
|
||||||
|
|---------|----------|
|
||||||
|
| `p` | Get current AZ/EL position |
|
||||||
|
| `P <az> <el>` | Move dish to target position |
|
||||||
|
| `S` | Stop tracking |
|
||||||
|
| `_` | Return model name |
|
||||||
|
| `q` | Disconnect |
|
||||||
|
|
||||||
|
For the Carryout G2, the server also supports extended commands: `R` (read RSSI), `L` (enable LNA), and `D` (discover capabilities).
|
||||||
|
|
||||||
|
4. **Configure Gpredict**
|
||||||
|
|
||||||
|
In Gpredict, add a rotor controller:
|
||||||
|
|
||||||
|
- **Host:** `127.0.0.1`
|
||||||
|
- **Port:** `4533`
|
||||||
|
- **Az type:** `0 -> 180 -> 360`
|
||||||
|
- **Min elevation:** set per your variant (see table below)
|
||||||
|
- **Max elevation:** set per your variant
|
||||||
|
|
||||||
|
| Variant | Min Elevation | Max Elevation |
|
||||||
|
|---------|--------------|--------------|
|
||||||
|
| Trav'ler (HAL 0.0.00) | 15 deg | 90 deg |
|
||||||
|
| Trav'ler (HAL 2.05) | 15 deg | 90 deg |
|
||||||
|
| Trav'ler Pro | 12 deg | 75 deg |
|
||||||
|
| Carryout (2003) | 22 deg | 73 deg |
|
||||||
|
| Carryout G2 | 18 deg | 65 deg |
|
||||||
|
|
||||||
|
The firmware enforces these limits. Commands below the minimum elevation are clamped by `birdcage` before being sent to the dish.
|
||||||
|
|
||||||
|
5. **Track a satellite**
|
||||||
|
|
||||||
|
Open Gpredict's satellite list, select a target with an upcoming pass, and click **Track**. Gpredict will start sending `P` (position) commands to the rotctld server at its configured update interval.
|
||||||
|
|
||||||
|
In the terminal running `birdcage serve`, you'll see the motor commands flowing:
|
||||||
|
|
||||||
|
```
|
||||||
|
14:23:01 INFO birdcage.antenna: Moving to AZ=145.3 EL=23.7 (move #0)
|
||||||
|
14:23:02 INFO birdcage.antenna: Moving to AZ=145.8 EL=24.1 (move #1)
|
||||||
|
14:23:03 INFO birdcage.antenna: Moving to AZ=146.2 EL=24.5 (move #2)
|
||||||
|
```
|
||||||
|
|
||||||
|
The leap-frog algorithm adds a small predictive overshoot to each command, compensating for mechanical lag. Motor commands alternate between AZ-first and EL-first to prevent either axis from starving on the shared serial bus.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## What's happening under the hood
|
||||||
|
|
||||||
|
When Gpredict sends `P 145.3 23.7`, here's the chain:
|
||||||
|
|
||||||
|
1. **RotctldServer** receives the TCP command and calls `antenna.move_to(145.3, 23.7)`
|
||||||
|
2. **BirdcageAntenna** applies leap-frog compensation: if the dish is currently at 144.0 AZ and moving right, the target gets nudged to ~145.8
|
||||||
|
3. **BirdcageAntenna** enforces the elevation floor (e.g., 18 deg for G2)
|
||||||
|
4. **BirdcageAntenna** alternates motor order -- even moves send AZ then EL, odd moves send EL then AZ
|
||||||
|
5. **FirmwareProtocol** sends `a 0 145.8` then `a 1 23.7` over serial
|
||||||
|
6. The dish firmware queues each motor command and executes them
|
||||||
|
|
||||||
|
The `a <id> <deg>` command is tolerant of rapid command streams -- it queues the new target while the motor is still moving to the previous one. This is what makes Gpredict's frequent updates work without overwhelming the firmware.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Dish doesn't move:**
|
||||||
|
- Is the TV search still running? Check that you disabled it (step 1).
|
||||||
|
- Is the dish initialized? Run `birdcage init` before `serve`.
|
||||||
|
- Are you in the motor submenu? The server handles this, but if you interrupted a previous session, the firmware may be in a different submenu.
|
||||||
|
|
||||||
|
**Position reads as 2147483647:**
|
||||||
|
- This is INT_MAX -- the motor axis is uncalibrated. The dish needs to home before it can report position. Power-cycle and let the boot sequence complete.
|
||||||
|
|
||||||
|
**Dish overshoots targets:**
|
||||||
|
- The leap-frog algorithm adds intentional overshoot. For fine pointing, you can disable it: `birdcage move --no-leapfrog --az 180 --el 45`.
|
||||||
|
|
||||||
|
**Connection drops after a few minutes:**
|
||||||
|
- Check your cable connections. RS-485/422 differential signaling is robust but loose wires cause intermittent failures.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Disabling Search Mode"
|
||||||
|
href="/guides/disabling-search/"
|
||||||
|
description="Detailed guide to permanently or temporarily stopping the TV satellite search on each variant."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Software Architecture"
|
||||||
|
href="/understanding/architecture/"
|
||||||
|
description="How the protocol, antenna, leapfrog, and rotctld layers fit together."
|
||||||
|
/>
|
||||||
111
src/content/docs/getting-started/index.mdx
Normal file
111
src/content/docs/getting-started/index.mdx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
title: What You Need
|
||||||
|
description: Hardware and software requirements for repurposing a Winegard dish for amateur radio satellite tracking.
|
||||||
|
sidebar:
|
||||||
|
order: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Card, CardGrid, Aside, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Before you start, you need three things: a dish, a way to talk to it, and the software to drive it. This page covers all three.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<Card title="A Winegard Dish" icon="star">
|
||||||
|
Any of the five supported variants will work:
|
||||||
|
|
||||||
|
- **Trav'ler** (HAL 0.0.00 or HAL 2.05) -- the most common, found on RV rooftops
|
||||||
|
- **Trav'ler Pro** -- USB A-to-A connection, requires ODU tunnel command
|
||||||
|
- **Carryout** (2003 model) -- portable dome, different motor protocol
|
||||||
|
- **Carryout G2** -- fully reverse-engineered, best documented variant
|
||||||
|
|
||||||
|
Surplus units are regularly available on eBay. The dish does not need a working receiver or subscription -- you only need the antenna unit and its power supply.
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Serial Adapter" icon="setting">
|
||||||
|
The adapter type depends on your dish variant:
|
||||||
|
|
||||||
|
- **Trav'ler (any HAL version):** USB-to-RS232 adapter + DTECH RS232-to-RS485 converter
|
||||||
|
- **Carryout G2:** USB-to-RS422 adapter (DSD TECH SH-U11 with FTDI FT232R, or equivalent)
|
||||||
|
- **Trav'ler Pro:** USB A-to-A cable (shows up as `ttyACM0`)
|
||||||
|
- **Carryout (2003):** USB-to-RS485 adapter (same as Trav'ler)
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Cables" icon="puzzle">
|
||||||
|
- **Trav'ler / Carryout 2003:** RJ-25 (6P6C) cable from IDU to your adapter. Standard phone cable works.
|
||||||
|
- **Carryout G2:** RJ-12 (6P6C) cable. Same physical connector, but uses all 4 signal wires (RS-422 full-duplex).
|
||||||
|
- A short run (under 3 meters) is fine without termination resistors.
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Power Supply" icon="rocket">
|
||||||
|
The dish needs 120VAC input to its RP-SK87 power supply, which outputs 12VDC to the IDU. The IDU in turn sends 12-18VDC bias through the coax to the LNB.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
<Aside type="caution" title="LNB Bias Voltage">
|
||||||
|
The internal coax carries 12-18VDC bias for the LNB. Do not connect 5V equipment (SDR LNAs, RTL-SDR dongles, etc.) directly to the dish's coax output without bypassing the power injector. You will damage the equipment.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Which adapter do I need?
|
||||||
|
|
||||||
|
The dish variants use two different serial standards. This matters when buying an adapter.
|
||||||
|
|
||||||
|
| Variant | Standard | Wires | Baud Rate | Recommended Adapter |
|
||||||
|
|---------|----------|-------|-----------|-------------------|
|
||||||
|
| Trav'ler (HAL 0.0.00) | RS-485 half-duplex | 2 + GND | 57600 | USB-to-RS232 + DTECH RS232-to-RS485 |
|
||||||
|
| Trav'ler (HAL 2.05) | RS-485 half-duplex | 2 + GND | 57600 | USB-to-RS232 + DTECH RS232-to-RS485 |
|
||||||
|
| Trav'ler Pro | USB direct | USB A-to-A | 57600 | USB A-to-A cable |
|
||||||
|
| Carryout (2003) | RS-485 half-duplex | 2 + GND | 57600 | USB-to-RS485 |
|
||||||
|
| Carryout G2 | RS-422 full-duplex | 4 + GND | 115200 | DSD TECH SH-U11 (FTDI FT232R) |
|
||||||
|
|
||||||
|
RS-485 half-duplex shares one differential pair for both transmit and receive. RS-422 uses separate pairs for each direction. The G2 talks at twice the baud rate of the other variants.
|
||||||
|
|
||||||
|
## Software
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<Card title="Python 3.10+" icon="seti:python">
|
||||||
|
The `birdcage` package requires Python 3.10 or later for type annotation support.
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="uv Package Manager" icon="seti:shell">
|
||||||
|
We use [uv](https://docs.astral.sh/uv/) for dependency management. Install it with:
|
||||||
|
```bash
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
```
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Gpredict (optional)" icon="seti:config">
|
||||||
|
[Gpredict](https://oz9aec.net/radios/digital-modes/gpredict-real-time-satellite-tracking-and-orbit-prediction-application) provides orbital prediction and real-time tracking via the rotctld protocol. Any Hamlib rotctld client works.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
### Installing the software
|
||||||
|
|
||||||
|
Clone the repository and install both packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-org/winegard-travler.git
|
||||||
|
cd winegard-travler
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs two CLI tools:
|
||||||
|
|
||||||
|
- **`birdcage`** -- antenna control, rotctld server, position queries, manual moves
|
||||||
|
- **`console-probe`** -- firmware exploration and command discovery tool
|
||||||
|
|
||||||
|
Verify the installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run birdcage --help
|
||||||
|
uv run console-probe --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Your First Connection"
|
||||||
|
href="/getting-started/first-connection/"
|
||||||
|
description="Plug in the adapter, find the serial port, and talk to the firmware console."
|
||||||
|
/>
|
||||||
269
src/content/docs/guides/ble-bridge.mdx
Normal file
269
src/content/docs/guides/ble-bridge.mdx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
---
|
||||||
|
title: "BLE-to-RS422 Bridge"
|
||||||
|
description: Building a wireless BLE bridge for the Carryout G2 using an ESP32-S3 and two MAX485 modules
|
||||||
|
sidebar:
|
||||||
|
order: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This guide covers building a transparent BLE-to-RS422 bridge for the Winegard Carryout G2 satellite dish. The bridge replaces the USB-to-RS422 adapter with a wireless connection using Bluetooth Low Energy (BLE) via the Nordic UART Service (NUS). Optional IMU and barometric sensors add orientation feedback and atmospheric refraction correction for improved tracking accuracy.
|
||||||
|
|
||||||
|
## Parts list
|
||||||
|
|
||||||
|
### Bridge (required)
|
||||||
|
|
||||||
|
- ESP32-S3-DevKitC-1-N16R8
|
||||||
|
- 2x MAX485 TTL-to-RS485 module
|
||||||
|
- 1x SparkFun Bidirectional Logic Level Converter (BOB-12009, BSS138-based)
|
||||||
|
- RJ-12 6P6C straight-wired cable with breakout
|
||||||
|
- Hookup wire / jumpers
|
||||||
|
|
||||||
|
### Sensors (optional)
|
||||||
|
|
||||||
|
- 1x GY-9250 (MPU-9250) -- 9-axis IMU (accelerometer + gyroscope + magnetometer)
|
||||||
|
- 1x BMP388 -- barometric pressure + temperature
|
||||||
|
- 1x RYS352A GPS module -- observer location + PPS timing
|
||||||
|
|
||||||
|
## Build procedure
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Wire the power rails.** The ESP32's 5V output powers the level converter's high side and both MAX485 modules. The 3.3V output powers the level converter's low side. All grounds must be connected together.
|
||||||
|
|
||||||
|
2. **Wire the level converter.** Connect ESP32 GPIO17 (TX) to LV1 and ESP32 GPIO18 (RX) to LV2 on the SparkFun converter. Connect HV1 to MAX485 board 1 DI, and HV2 to MAX485 board 2 RO.
|
||||||
|
|
||||||
|
3. **Lock the TX module (Board 1).** Tie DE and RE on Board 1 to 5V. This permanently enables the driver and disables the receiver -- the board only transmits.
|
||||||
|
|
||||||
|
4. **Lock the RX module (Board 2).** Tie DE and RE on Board 2 to GND. This permanently disables the driver and enables the receiver -- the board only receives.
|
||||||
|
|
||||||
|
5. **Wire the RJ-12 breakout.** Connect Board 1's A/B to pins 2/3 (TX pair) and Board 2's A/B to pins 4/5 (RX pair). Connect pin 1 to common ground.
|
||||||
|
|
||||||
|
6. **Wire the I2C sensors (optional).** Connect MPU-9250 and BMP388 to GPIO8 (SDA) and GPIO9 (SCL) on the shared 3.3V I2C bus.
|
||||||
|
|
||||||
|
7. **Wire the GPS module (optional).** Connect RYS352A TX to GPIO5 (UART2 RX), RX to GPIO6 (UART2 TX), and PPS to GPIO7.
|
||||||
|
|
||||||
|
8. **Flash the firmware.** See `firmware/ble-bridge/` in the project repository.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Schematic
|
||||||
|
|
||||||
|
### Main bridge circuit
|
||||||
|
|
||||||
|
```
|
||||||
|
SparkFun Level Converter (BOB-12009)
|
||||||
|
+--------------------------------------+
|
||||||
|
| |
|
||||||
|
ESP32 3V3 --------------+| LV HV |+-- ESP32 5V
|
||||||
|
ESP32 GND --------------+| GND GND |+-- (shared)
|
||||||
|
| |
|
||||||
|
ESP32 GPIO17 (TX) ------+| LV1 HV1 |+-----> MAX485_1 DI
|
||||||
|
ESP32 GPIO18 (RX) +------| LV2 HV2 |+---- MAX485_2 RO
|
||||||
|
| |
|
||||||
|
| LV3 (spare) HV3 (spare) |
|
||||||
|
| LV4 (spare) HV4 (spare) |
|
||||||
|
+--------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
MAX485 Board 1 (TX only) MAX485 Board 2 (RX only)
|
||||||
|
+------------------------+ +------------------------+
|
||||||
|
| VCC <-- 5V | | VCC <-- 5V |
|
||||||
|
| GND <-- GND | | GND <-- GND |
|
||||||
|
| | | |
|
||||||
|
| DI <-- HV1 | | RO --> HV2 |
|
||||||
|
| RO (unused) | | DI (unused) |
|
||||||
|
| | | |
|
||||||
|
| DE <-- 5V | locked | | DE <-- GND | locked |
|
||||||
|
| RE <-- 5V | TX mode | | RE <-- GND | RX mode |
|
||||||
|
| | | |
|
||||||
|
| A ---------------------+--> pin 2 | A <---------------------+-- pin 4
|
||||||
|
| B ---------------------+--> pin 3 | B <---------------------+-- pin 5
|
||||||
|
+------------------------+ +------------------------+
|
||||||
|
|
||||||
|
RJ-12 to Carryout G2
|
||||||
|
+---------------------------+
|
||||||
|
| Pin 1 (White) -- GND |<-- ESP32 GND
|
||||||
|
| Pin 2 (Red) -- TX+/TA |<-- A_1
|
||||||
|
| Pin 3 (Black) -- TX-/TB |<-- B_1
|
||||||
|
| Pin 4 (Yellow) -- RX+/RA |--> A_2
|
||||||
|
| Pin 5 (Green) -- RX-/RB |--> B_2
|
||||||
|
| Pin 6 (Blue) -- N/C |
|
||||||
|
+---------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Power rails
|
||||||
|
|
||||||
|
```
|
||||||
|
ESP32 5V --+-- Level Converter HV
|
||||||
|
+-- MAX485_1 VCC
|
||||||
|
+-- MAX485_1 DE + RE (tied high = TX mode)
|
||||||
|
+-- MAX485_2 VCC
|
||||||
|
|
||||||
|
ESP32 3V3 --- Level Converter LV
|
||||||
|
|
||||||
|
ESP32 GND -+-- Level Converter GND
|
||||||
|
+-- MAX485_1 GND
|
||||||
|
+-- MAX485_2 GND
|
||||||
|
+-- MAX485_2 DE + RE (tied low = RX mode)
|
||||||
|
+-- RJ-12 Pin 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The Carryout G2 uses RS-422 full-duplex: two separate differential pairs, one for each direction. The MAX485 is a half-duplex RS-485 transceiver with a shared A/B pair and direction control pins (DE/RE). By hardwiring DE/RE, each board is locked into a single direction:
|
||||||
|
|
||||||
|
- **Board 1 (TX):** DE=HIGH, RE=HIGH -- driver always enabled, receiver disabled. ESP32 UART1 TX goes through the level shifter to DI, out as differential A/B to the G2 serial RX.
|
||||||
|
|
||||||
|
- **Board 2 (RX):** DE=LOW, RE=LOW -- driver disabled, receiver always enabled. G2 serial TX comes in on differential A/B, through RO and the level shifter to ESP32 UART1 RX.
|
||||||
|
|
||||||
|
The SparkFun level converter translates between 3.3V (ESP32) and 5V (MAX485) on both data lines. The two spare channels (LV3/HV3, LV4/HV4) are available if DE/RE ever need GPIO control for a half-duplex variant.
|
||||||
|
|
||||||
|
The firmware is the same regardless of whether the RS-422 transceiver is a MAX490 (single full-duplex chip) or two MAX485s (locked half-duplex pair). It only sees UART TX/RX on GPIO17/18.
|
||||||
|
|
||||||
|
## RJ-12 cable notes
|
||||||
|
|
||||||
|
Straight-wired 6P6C. Pin 1 is leftmost when looking at the jack with the clip facing away from you (tab down).
|
||||||
|
|
||||||
|
| Pin | Color | Function | Connects to |
|
||||||
|
|-----|--------|------------------|--------------------|
|
||||||
|
| 1 | White | GND | Common ground |
|
||||||
|
| 2 | Red | TX+ (TA) | MAX485 Board 1 A |
|
||||||
|
| 3 | Black | TX- (TB) | MAX485 Board 1 B |
|
||||||
|
| 4 | Yellow | RX+ (RA) | MAX485 Board 2 A |
|
||||||
|
| 5 | Green | RX- (RB) | MAX485 Board 2 B |
|
||||||
|
| 6 | Blue | N/C | -- |
|
||||||
|
|
||||||
|
If crimping your own cable, verify pin-to-color with a multimeter before connecting to the dish. RJ-12 crimps are easy to get reversed (pins mirror if the connector is flipped). A wrong connection won't damage anything (differential signals are current-limited) but communication won't work.
|
||||||
|
|
||||||
|
## Sensors -- I2C bus
|
||||||
|
|
||||||
|
The MPU-9250 and BMP388 share a single I2C bus on GPIO8 (SDA) / GPIO9 (SCL). Both run at 3.3V directly from the ESP32, no level shifting needed.
|
||||||
|
|
||||||
|
### I2C bus schematic
|
||||||
|
|
||||||
|
```
|
||||||
|
I2C Bus (3.3V, 400kHz)
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
ESP32 3V3 --+------------------+--- MPU-9250 VCC
|
||||||
|
| +--- BMP388 VCC
|
||||||
|
|
|
||||||
|
+-- 4.7K_ohm -- SDA bus --+-- MPU-9250 SDA
|
||||||
|
| +-- BMP388 SDI
|
||||||
|
|
|
||||||
|
+-- 4.7K_ohm -- SCL bus --+-- MPU-9250 SCL
|
||||||
|
+-- BMP388 SCK
|
||||||
|
|
||||||
|
ESP32 GPIO8 (SDA) ---- SDA bus
|
||||||
|
ESP32 GPIO9 (SCL) ---- SCL bus
|
||||||
|
|
||||||
|
ESP32 GND --+-- MPU-9250 GND
|
||||||
|
+-- BMP388 GND (SDO to GND = addr 0x76)
|
||||||
|
|
||||||
|
MPU-9250 AD0 -- GND (I2C address = 0x68)
|
||||||
|
BMP388 SDO -- GND (I2C address = 0x76)
|
||||||
|
```
|
||||||
|
|
||||||
|
The 4.7K-ohm pull-ups are shared -- one pair for the whole bus. Many breakout boards include onboard pull-ups already; if both the GY-9250 and BMP388 boards have them, the combined parallel resistance (~2.3K-ohm) is still fine for 400kHz I2C at 3.3V. Only add external pull-ups if neither board has them.
|
||||||
|
|
||||||
|
### MPU-9250 (GY-9250) -- 9-axis IMU
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| I2C Address | 0x68 (AD0 to GND) |
|
||||||
|
| VCC | 3-5V (onboard LDO) |
|
||||||
|
| Interface | I2C (up to 400kHz) or SPI |
|
||||||
|
|
||||||
|
What it provides for satellite tracking:
|
||||||
|
|
||||||
|
- **Magnetometer (AK8963):** Compass heading for automatic north alignment. Eliminates manual alignment of dish base "BACK" marking to true north. Apply local magnetic declination to convert magnetic north to true north.
|
||||||
|
- **Accelerometer:** Gravity vector gives tilt angle = elevation. Independent verification of the dish firmware's reported EL position.
|
||||||
|
- **Gyroscope:** Angular rate during slews. Detect oscillation, overshoot, and vibration for tuning the leapfrog overshoot compensation algorithm.
|
||||||
|
|
||||||
|
<Aside type="tip" title="Mounting matters">
|
||||||
|
The magnetometer is extremely sensitive to nearby ferrous metals and electromagnetic interference from motors. Mount on the fixed base plate, away from motor housings, with a known axis aligned to the dish's reference direction. Rigid mounting -- any flex between sensor and dish structure introduces measurement error.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### BMP388 -- barometric pressure + temperature
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| I2C Address | 0x76 (SDO to GND) |
|
||||||
|
| VCC | 3.3V |
|
||||||
|
| Pressure range | 300-1250 hPa |
|
||||||
|
| Pressure resolution | +/-0.01 hPa (+/-8 cm altitude) |
|
||||||
|
| Temperature accuracy | +/-0.5 degrees C |
|
||||||
|
| Interface | I2C (up to 3.4MHz) or SPI |
|
||||||
|
|
||||||
|
What it provides for satellite tracking:
|
||||||
|
|
||||||
|
- **Atmospheric refraction correction.** Radio signals bend as they pass through the atmosphere, especially at low elevation angles. The amount of bending depends on air pressure and temperature. At 15 degrees elevation (the Trav'ler's minimum), refraction shifts apparent position by ~0.2 degrees. Standard refraction models (Bennett, Saemundsson) take pressure and temperature as inputs -- the BMP388 provides both in real time.
|
||||||
|
- **Temperature monitoring.** Ambient temperature at the dish for thermal drift awareness and electronics health monitoring.
|
||||||
|
|
||||||
|
The simplified Bennett refraction formula:
|
||||||
|
|
||||||
|
```
|
||||||
|
R = 1/tan(el + 7.31/(el + 4.4)) * (P/1010) * (283/(273 + T))
|
||||||
|
```
|
||||||
|
|
||||||
|
Where R is refraction in arcminutes, el is apparent elevation in degrees, P is pressure in hPa, T is temperature in degrees C. At el=15, P=1013, T=20: R is approximately 3.4 arcmin (0.057 degrees). Small but meaningful for narrow-beam antennas.
|
||||||
|
|
||||||
|
## GPS -- RYS352A
|
||||||
|
|
||||||
|
The RYS352A is a compact GPS module with PPS output. It connects via UART2 and provides observer location for satellite pass prediction and a 1Hz PPS pulse for precise UTC time synchronization.
|
||||||
|
|
||||||
|
### GPS wiring
|
||||||
|
|
||||||
|
```
|
||||||
|
ESP32 GPIO5 (UART2 RX) <-- RYS352A TX (NMEA sentences out)
|
||||||
|
ESP32 GPIO6 (UART2 TX) --> RYS352A RX (config commands in, optional)
|
||||||
|
ESP32 GPIO7 <-- RYS352A PPS (1Hz rising edge, ~100ns jitter)
|
||||||
|
ESP32 3V3 --> RYS352A VCC
|
||||||
|
ESP32 GND --> RYS352A GND
|
||||||
|
```
|
||||||
|
|
||||||
|
| Module Pin | ESP32 Pin | Function |
|
||||||
|
|------------|-----------|----------|
|
||||||
|
| VCC | 3V3 | 3.3V power (onboard LDO on most breakouts) |
|
||||||
|
| GND | GND | Ground |
|
||||||
|
| TX | GPIO5 (UART2 RX) | NMEA sentence output at 115200 baud |
|
||||||
|
| RX | GPIO6 (UART2 TX) | PAIR/NMEA config input (optional) |
|
||||||
|
| PPS | GPIO7 | 1Hz pulse synchronized to GPS time |
|
||||||
|
|
||||||
|
**PPS (Pulse Per Second):** The RYS352A outputs a precise 1Hz pulse on the rising edge, synchronized to UTC via GPS constellation. The firmware captures this edge via interrupt for correlating satellite events with sub-microsecond precision. The module's RTC battery backup enables warm starts (~5s) after initial cold start fix (~30-60s).
|
||||||
|
|
||||||
|
**UART notes:** The RYS352A defaults to 115200 baud NMEA output with `GN` talker ID (multi-constellation). The TX line (GPIO6) sends `$PAIR` proprietary commands at boot to configure the GPS module for satellite tracking use:
|
||||||
|
|
||||||
|
- Only GGA (position/quality), GSA (fix mode/DOP), RMC (time/date/speed), and GSV (satellite visibility, every 5th fix) are enabled
|
||||||
|
- Redundant sentences (GLL, VTG, ZDA, GRS, GST, GNS) are disabled to reduce parser load and latency
|
||||||
|
- PPS is configured to pulse only on 2D/3D fix with 100ms pulse width
|
||||||
|
- Each command waits for `$PAIR001` ACK; failures are logged but non-fatal
|
||||||
|
|
||||||
|
## Full GPIO map
|
||||||
|
|
||||||
|
| GPIO | Function | Interface | Notes |
|
||||||
|
|------|----------|-----------|-------|
|
||||||
|
| 5 | GPS RX | UART2 RX | From RYS352A TX (NMEA out) |
|
||||||
|
| 6 | GPS TX | UART2 TX | To RYS352A RX (config in) |
|
||||||
|
| 7 | GPS PPS | GPIO interrupt | 1Hz rising edge |
|
||||||
|
| 8 | I2C SDA | I2C | MPU-9250 + BMP388 (shared bus) |
|
||||||
|
| 9 | I2C SCL | I2C | MPU-9250 + BMP388 (shared bus) |
|
||||||
|
| 17 | RS-422 TX | UART1 TX | To level shifter to MAX485 Board 1 DI |
|
||||||
|
| 18 | RS-422 RX | UART1 RX | From level shifter from MAX485 Board 2 RO |
|
||||||
|
| 38 | RGB LED | WS2812 | Onboard NeoPixel (DevKitC V1.1) |
|
||||||
|
| 43 | USB Console TX | UART0 | CH343 USB-serial (untouched) |
|
||||||
|
| 44 | USB Console RX | UART0 | CH343 USB-serial (untouched) |
|
||||||
|
|
||||||
|
## Loopback test
|
||||||
|
|
||||||
|
<Aside type="tip" title="Test before connecting to the dish">
|
||||||
|
Before connecting to the G2, verify the bridge by shorting MAX485 Board 1 A to MAX485 Board 2 A, and MAX485 Board 1 B to MAX485 Board 2 B (loop TX back into RX). Anything sent via BLE or USB serial should echo back. This confirms:
|
||||||
|
|
||||||
|
- The level converter is translating correctly between 3.3V and 5V
|
||||||
|
- Both MAX485 modules are in the correct mode (TX-only and RX-only)
|
||||||
|
- The ESP32 UART1 is configured at the right baud rate
|
||||||
|
- The BLE Nordic UART Service is passing data transparently
|
||||||
|
|
||||||
|
If the loopback works but the dish doesn't respond after connecting, check the RJ-12 wiring -- particularly the TX+/TX- polarity. Swapping the TX pair causes silent failure (the dish can't decode the inverted framing).
|
||||||
|
</Aside>
|
||||||
161
src/content/docs/guides/calibration.mdx
Normal file
161
src/content/docs/guides/calibration.mdx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
---
|
||||||
|
title: "Calibration & Homing"
|
||||||
|
description: How the dish establishes its position reference and what to do when it gets lost
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Winegard dishes use stepper motors with no absolute position encoders. They determine their position by **homing** -- driving each axis until a mechanical stall is detected, then counting steps from that reference point. If the position reference is lost (power failure, missed steps, uncalibrated boot), the firmware has no idea where the dish is pointing.
|
||||||
|
|
||||||
|
## Power-up calibration
|
||||||
|
|
||||||
|
On a normal power-up with the tracker enabled, the dish runs a full calibration sequence automatically:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Bootloader initializes.** Firmware loads, peripherals (SPI for motor drivers, SPI for DVB tuner) are configured.
|
||||||
|
|
||||||
|
2. **Elevation homes first.** The EL motor drives upward until it stalls against the mechanical hard stop. The firmware uses a 2-second stall detection timeout. This is the EL reference position (65.00 degrees on the G2, per NVS index 103).
|
||||||
|
|
||||||
|
3. **Azimuth homes second.** The AZ motor drives in one direction until it stalls (8-second timeout). This establishes the AZ reference and cable wrap limits.
|
||||||
|
|
||||||
|
4. **Cable wrap limits are set.** On the Carryout G2, homing output reports `wrap_min:-42333 wrap_max:2333` (centidegrees), giving a total range of approximately 446.66 degrees.
|
||||||
|
|
||||||
|
5. **TV satellite search begins.** The firmware starts scanning for DirecTV/DISH satellites. This is where you [disable the search](/guides/disabling-search/).
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="caution" title="Expect grinding sounds">
|
||||||
|
The Carryout and Carryout G2 use motor stalling (not limit switches) to detect mechanical boundaries. The stall detection works by driving the motor at a known current and watching for the step position to stop advancing. You will hear audible grinding during this process -- this is normal and expected. The original Carryout calibration takes approximately 10-15 minutes. The G2 is faster.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Carryout G2 boot sequence (detailed)
|
||||||
|
|
||||||
|
The G2's boot process is well-documented from serial capture:
|
||||||
|
|
||||||
|
1. Bootloader v1.01 starts
|
||||||
|
2. SPI1 init at 4 MHz -- configures the two A3981 stepper motor driver ICs (mode 0x03)
|
||||||
|
3. Motor init -- System=12Inch, master=40000 steps/rev (AZ), slave=24960 steps/rev (EL), ratio=1.602564
|
||||||
|
4. SPI2 init at 6.857 MHz -- configures the BCM4515 DVB tuner (mode 0x03)
|
||||||
|
5. `EXTENDED_DVB_DEBUG ENABLED`
|
||||||
|
6. DVB init -- AP RAM FW verified, BCM4515 ID 0x4515 Rev B0, FW v113.37, strap 0x25018
|
||||||
|
7. Auto-search config -- blind scan, 18000-24000 ksps, rolloff 0.35
|
||||||
|
8. `Enabled LNB STB`
|
||||||
|
9. `Ant ID - 12-IN G2`
|
||||||
|
10. NVS load from flash
|
||||||
|
11. EL home (stall detect, 2s timeout)
|
||||||
|
12. AZ home (stall detect, 8s timeout)
|
||||||
|
13. `Antenna Facing Front`
|
||||||
|
14. `TRK>` prompt appears (if tracker disabled) or search starts
|
||||||
|
|
||||||
|
## When NVS 20 is TRUE (tracker disabled)
|
||||||
|
|
||||||
|
If you've set NVS 20 to TRUE to [permanently disable the TV search](/guides/disabling-search/), the firmware skips the homing sequence entirely. The motors stay uncalibrated, and the AZ position register contains **2147483647** (INT_MAX) -- a sentinel value meaning "position unknown."
|
||||||
|
|
||||||
|
This means you must home the motors manually before issuing any move commands.
|
||||||
|
|
||||||
|
### Manual homing (Carryout G2)
|
||||||
|
|
||||||
|
The G2 has an explicit `h <id>` homing command in the motor submenu:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> mot
|
||||||
|
MOT> h 0
|
||||||
|
```
|
||||||
|
|
||||||
|
This homes motor 0 (azimuth). Wait for it to complete -- the motor will drive until it stalls, then report its reference position.
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> h 1
|
||||||
|
```
|
||||||
|
|
||||||
|
This homes motor 1 (elevation). Same stall-detect process, shorter timeout (2s vs 8s for AZ).
|
||||||
|
|
||||||
|
After both motors are homed, position queries with `a` will return valid angles instead of INT_MAX.
|
||||||
|
|
||||||
|
<Aside type="caution" title="Do not skip homing">
|
||||||
|
Running motor commands on an uncalibrated axis is dangerous. The firmware has no idea where the dish is -- commanding `a 0 180` when the position register says 2147483647 may attempt to drive the motor billions of steps, deadlocking the shell and requiring a power cycle. The ADC `scan` command is especially hazardous on uncalibrated axes.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## EL recalibration via IDU buttons
|
||||||
|
|
||||||
|
On the Trav'ler (with the indoor display unit), elevation can be recalibrated through the IDU menu:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Press **POWER** to turn on the IDU.
|
||||||
|
|
||||||
|
2. Press and hold **ENTER** for 2 seconds to open the User Menu.
|
||||||
|
|
||||||
|
3. Navigate to **INSTALLATION**.
|
||||||
|
|
||||||
|
4. Select **Calibrate EL**.
|
||||||
|
|
||||||
|
5. Confirm the hard stop position when prompted.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
This re-establishes the EL reference by driving to the mechanical stop, similar to the automatic homing but initiated manually through the user interface.
|
||||||
|
|
||||||
|
## Cable wrap protection
|
||||||
|
|
||||||
|
The azimuth motor has a limited rotational range before the internal cables wrap too tightly and risk damage. The firmware tracks this and reverses direction at the wrap limits.
|
||||||
|
|
||||||
|
On the Carryout G2, confirmed wrap limits are:
|
||||||
|
|
||||||
|
| Parameter | Value | Degrees |
|
||||||
|
|-----------|-------|---------|
|
||||||
|
| `wrap_min` | -42333 centidegrees | -423.33 degrees |
|
||||||
|
| `wrap_max` | 2333 centidegrees | 23.33 degrees |
|
||||||
|
| **Total range** | 44666 centidegrees | **~446.66 degrees** |
|
||||||
|
|
||||||
|
The wrap manager can be queried and controlled from the motor submenu:
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> w 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows the current wrap status for motor 0 (AZ). Use `w 0 ON` or `w 0 OFF` to enable or disable wrap protection (disabling is not recommended).
|
||||||
|
|
||||||
|
The SK-1000 (full-size Trav'ler) has a wrap range of 0-455 degrees.
|
||||||
|
|
||||||
|
## Elevation limits
|
||||||
|
|
||||||
|
Each variant has firmware-enforced elevation limits:
|
||||||
|
|
||||||
|
| Variant | Min Elevation | Max Elevation |
|
||||||
|
|---------|--------------|--------------|
|
||||||
|
| HAL 0.0.00 | 15 degrees | 90 degrees |
|
||||||
|
| HAL 2.05 | 15 degrees | 90 degrees |
|
||||||
|
| Trav'ler Pro | 12 degrees | 75 degrees (hardware cap) |
|
||||||
|
| Carryout (2003) | 22 degrees | 73 degrees (NVS 102 override) |
|
||||||
|
| Carryout G2 | 18 degrees | 65 degrees |
|
||||||
|
|
||||||
|
On the G2, these are stored in NVS indices 101 (min) and 102 (max). They can be read with:
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> elminmaxhome
|
||||||
|
Min: 18.00 Max: 65.00 Home: 65.00
|
||||||
|
```
|
||||||
|
|
||||||
|
## Emergency manual stow
|
||||||
|
|
||||||
|
Last resort only, for when the dish is deployed and firmware is unresponsive.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Get a **5/16" socket** with a **6" extension**.
|
||||||
|
|
||||||
|
2. Insert the socket into the **auxiliary drive hole** on the motor assembly.
|
||||||
|
|
||||||
|
3. Turn **clockwise slowly** to drive the arm down.
|
||||||
|
|
||||||
|
4. **Ensure the arm faces the "rear" label** before lowering to avoid collision with the base.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="caution" title="Risk of motor damage">
|
||||||
|
Improper execution of manual stow can strip the motor gearing or bend the arm. Only use this procedure when you cannot power the dish or command a software stow. The firmware `stow` command is always preferred.
|
||||||
|
</Aside>
|
||||||
143
src/content/docs/guides/disabling-search.mdx
Normal file
143
src/content/docs/guides/disabling-search.mdx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
title: "Disabling TV Search"
|
||||||
|
description: How to prevent the dish from automatically searching for TV satellites on boot
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
On power-up, every Winegard dish variant attempts to calibrate and then search for DirecTV or DISH Network satellites. For amateur radio use, this search must be killed -- it fights your tracking commands and wastes time scanning the sky for TV signals you don't need.
|
||||||
|
|
||||||
|
Each firmware variant has a different method for stopping the search. Some are temporary (per-boot), others are permanent (written to non-volatile storage).
|
||||||
|
|
||||||
|
## Per-variant instructions
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="HAL 0.0.00">
|
||||||
|
|
||||||
|
### Trav'ler (HAL 0.0.00)
|
||||||
|
|
||||||
|
The original Trav'ler uses the `os` (operating system) submenu to kill the search task.
|
||||||
|
|
||||||
|
```
|
||||||
|
> os
|
||||||
|
OS> kill Search
|
||||||
|
```
|
||||||
|
|
||||||
|
This terminates the `Search` task for the current session. You need to repeat this on every power cycle.
|
||||||
|
|
||||||
|
**Sequence:**
|
||||||
|
1. Wait for the boot prompt (`>` or `NoGPS` message)
|
||||||
|
2. Enter `os` to open the OS submenu
|
||||||
|
3. Send `kill Search` to stop the satellite search task
|
||||||
|
4. Send `q` to return to the root menu
|
||||||
|
5. Enter `mot` to access motor control
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="HAL 2.05">
|
||||||
|
|
||||||
|
### Trav'ler (HAL 2.05.003)
|
||||||
|
|
||||||
|
HAL 2.05 uses a different path through the `ngsearch` submenu.
|
||||||
|
|
||||||
|
```
|
||||||
|
> ngsearch
|
||||||
|
NGSEARCH> s
|
||||||
|
NGSEARCH> q
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sequence:**
|
||||||
|
1. Wait for boot (look for `NoGPS` or `No LNB Voltage`)
|
||||||
|
2. Enter `ngsearch` to open the search submenu
|
||||||
|
3. Send `s` to stop the current search
|
||||||
|
4. Send `q` to exit back to root
|
||||||
|
5. Enter `mot` (or `motor` on some HAL 2.05 builds) to access motor control
|
||||||
|
|
||||||
|
This is a per-boot operation -- the search restarts on every power cycle.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Trav'ler Pro">
|
||||||
|
|
||||||
|
### Trav'ler Pro
|
||||||
|
|
||||||
|
The Pro uses the same `os` / `kill Search` method as HAL 0.0.00, but you may need to tunnel to the outdoor unit first since the Pro's IDU has its own MCU.
|
||||||
|
|
||||||
|
```
|
||||||
|
> os
|
||||||
|
OS> kill Search
|
||||||
|
```
|
||||||
|
|
||||||
|
If the kill doesn't stop dish movement, try tunneling to the ODU first:
|
||||||
|
|
||||||
|
```
|
||||||
|
> odu
|
||||||
|
ODU> os
|
||||||
|
OS> kill Search
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a per-boot operation.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Carryout">
|
||||||
|
|
||||||
|
### Carryout (2003)
|
||||||
|
|
||||||
|
The original Carryout doesn't have a firmware command to kill the search. Instead, it uses **physical DIP switches** on the unit.
|
||||||
|
|
||||||
|
Setting all DIP switches to **off (up)** may disable search mode, but behavior varies by unit. There is no guaranteed software method.
|
||||||
|
|
||||||
|
<Aside type="caution" title="Inconsistent behavior">
|
||||||
|
DIP switch behavior is not consistent across Carryout units. Some units ignore the switches entirely. If the dish continues searching after flipping all switches off, you may need to wait for the search to complete before taking motor control.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="Carryout G2">
|
||||||
|
|
||||||
|
### Carryout G2
|
||||||
|
|
||||||
|
The G2 provides a **permanent** disable via non-volatile storage index 20. Once set, the tracker process is disabled across reboots.
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> nvs
|
||||||
|
NVS> e 20 1
|
||||||
|
NVS> s
|
||||||
|
NVS> q
|
||||||
|
```
|
||||||
|
|
||||||
|
**Breakdown:**
|
||||||
|
1. `nvs` -- enter the NVS submenu
|
||||||
|
2. `e 20 1` -- set index 20 ("Disable Tracker Proc?") to TRUE (1)
|
||||||
|
3. `s` -- save the change to flash (required -- without this, the change is lost on reboot)
|
||||||
|
4. `q` -- return to root menu
|
||||||
|
|
||||||
|
To re-enable the tracker later:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> nvs
|
||||||
|
NVS> e 20 0
|
||||||
|
NVS> s
|
||||||
|
NVS> q
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="caution" title="Homing is skipped when tracker is disabled">
|
||||||
|
When NVS 20 = TRUE, the firmware skips the homing sequence entirely on boot. The motors stay uncalibrated and the AZ position reads as **2147483647** (INT_MAX sentinel). You must manually home the motors with `mot` then `h 0` (AZ) and `h 1` (EL) before issuing any move commands. See the [Calibration & Homing](/guides/calibration/) guide.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## What happens after disabling search
|
||||||
|
|
||||||
|
Once the search is killed, the dish stops moving on its own and waits for your commands. At this point you can:
|
||||||
|
|
||||||
|
- Enter the motor submenu (`mot` on most variants, `motor` on HAL 2.05, `target` on the original Carryout)
|
||||||
|
- Query the current position with `a`
|
||||||
|
- Issue move commands with `a <motor_id> <degrees>`
|
||||||
|
- Start the [rotctld server](/guides/satellite-tracking/) for Gpredict integration
|
||||||
|
|
||||||
|
The `birdcage init` command automates this entire sequence -- it waits for boot, kills the search, enters the motor submenu, and leaves the dish ready for tracking. See the [satellite tracking guide](/guides/satellite-tracking/) for the full workflow.
|
||||||
193
src/content/docs/guides/firmware-probing.mdx
Normal file
193
src/content/docs/guides/firmware-probing.mdx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
title: "Firmware Probing"
|
||||||
|
description: How to use the console-probe tool to discover and document firmware commands
|
||||||
|
sidebar:
|
||||||
|
order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The `console-probe` tool is a generic embedded console scanner built for reverse-engineering firmware command interfaces. It auto-detects the prompt format, parses help output, enters submenus, and optionally brute-forces candidate command names. It was developed for the Winegard firmware but works with any prompt-based serial console.
|
||||||
|
|
||||||
|
## How console-probe works
|
||||||
|
|
||||||
|
The discovery process follows a structured sequence:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Prompt detection.** The tool sends empty lines (carriage returns) and analyzes the response to identify the prompt string. On the Carryout G2, this detects `TRK>` as the root prompt.
|
||||||
|
|
||||||
|
2. **Error string detection.** A known-bad command is sent to capture the error response pattern. This lets the tool distinguish between "command recognized" and "command unknown" for subsequent probing.
|
||||||
|
|
||||||
|
3. **Help parsing.** The tool sends the help command (default: `?`) and parses the response to extract known commands, their parameters, and descriptions. It identifies which commands are submenus (they change the prompt when entered).
|
||||||
|
|
||||||
|
4. **Submenu discovery.** For each discovered submenu, the tool enters it, queries help, and records the available commands. The prompt changes (e.g., `TRK>` to `MOT>`) confirming successful submenu entry.
|
||||||
|
|
||||||
|
5. **Brute-force probing (optional).** With `--deep`, the tool sends candidate command names from a wordlist and checks whether each one produces a valid response (as opposed to the error string). This catches commands not listed in help output.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
### Discover commands via help only
|
||||||
|
|
||||||
|
The fastest mode -- queries `?` at each menu level, no brute-force:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --discover-only
|
||||||
|
```
|
||||||
|
|
||||||
|
This enters every discovered submenu, queries help, and prints the command inventory. Add `--json` to save a structured report:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --discover-only --json /tmp/discover.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deep probe with wordlist
|
||||||
|
|
||||||
|
Full discovery plus brute-force probing of all submenus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --deep --wordlist scripts/wordlists/winegard.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The wordlist file contains one candidate command per line. The bundled `winegard.txt` includes terms extracted from firmware strings and documentation.
|
||||||
|
|
||||||
|
### Probe a single submenu
|
||||||
|
|
||||||
|
Target a specific submenu without scanning all of them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --submenu dvb
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI options reference
|
||||||
|
|
||||||
|
### Connection options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--port` | `/dev/ttyUSB0` | Serial port |
|
||||||
|
| `--baud` | `115200` | Baud rate |
|
||||||
|
| `--line-ending` | `cr` | Line ending to send (`cr`, `lf`, `crlf`) |
|
||||||
|
|
||||||
|
### Discovery overrides
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--prompt` | auto-detect | Override the root prompt string |
|
||||||
|
| `--error` | auto-detect | Override the error response string |
|
||||||
|
| `--help-cmd` | `?` | Command to request help |
|
||||||
|
| `--exit-cmd` | `q` | Command to exit submenus |
|
||||||
|
|
||||||
|
### Probing options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--discover-only` | off | Help-only mode, no brute-force |
|
||||||
|
| `--deep` | off | Probe all discovered submenus |
|
||||||
|
| `--submenu NAME` | none | Probe a single named submenu |
|
||||||
|
| `--timeout` | `0.5` | Per-command timeout in seconds |
|
||||||
|
| `--blocklist` | `reboot,stow,def,q,Q` | Commands to never send |
|
||||||
|
| `--wordlist FILE` | none | Extra candidate words file (repeatable) |
|
||||||
|
|
||||||
|
### Output options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--json FILE` | none | Write results as JSON to file |
|
||||||
|
|
||||||
|
<Aside type="tip" title="Use ? in submenus manually">
|
||||||
|
Even without console-probe, you can explore the firmware interactively. Enter any submenu and type `?` to see its commands. On the G2's DVB submenu, help is paginated -- `?` shows the first page and `man` shows extended commands including DiSEqC controls.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## The JSON report
|
||||||
|
|
||||||
|
When `--json` is specified, the tool writes a structured report with format version 2. The report contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"device": {
|
||||||
|
"port": "/dev/ttyUSB2",
|
||||||
|
"baud": 115200,
|
||||||
|
"root_prompt": "TRK>",
|
||||||
|
"error_string": "type '?' for help"
|
||||||
|
},
|
||||||
|
"menus": {
|
||||||
|
"TRK": {
|
||||||
|
"help": [
|
||||||
|
{"name": "mot", "params": "", "description": "enter motor control submenu"},
|
||||||
|
{"name": "dvb", "params": "", "description": "enter DVB tuner submenu"}
|
||||||
|
],
|
||||||
|
"probe_hits": [],
|
||||||
|
"undiscovered": []
|
||||||
|
},
|
||||||
|
"MOT": {
|
||||||
|
"help": [...],
|
||||||
|
"probe_hits": [["a", "Angle[0] = 180.00 Angle[1] = 45.00"]],
|
||||||
|
"undiscovered": [["xyz", "some unexpected response"]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key sections in each menu:
|
||||||
|
|
||||||
|
- **`help`** -- Commands discovered via the `?` command, with parameter syntax and descriptions.
|
||||||
|
- **`probe_hits`** -- Commands that produced a valid (non-error) response during brute-force probing.
|
||||||
|
- **`undiscovered`** -- Commands found by probing that were *not* in the help output. These are the interesting findings -- hidden or undocumented commands.
|
||||||
|
|
||||||
|
## Practical tips
|
||||||
|
|
||||||
|
### Blocklist safety
|
||||||
|
|
||||||
|
The default blocklist (`reboot,stow,def,q,Q`) prevents the probe from sending commands that would disrupt the session. The `reboot` command restarts the firmware. The `stow` command folds the dish flat (dangerous if you've modified the feed). The `def` command restores factory defaults. And `q` exits the shell entirely on the G2 root menu (kills UART, requires power cycle).
|
||||||
|
|
||||||
|
Add more commands to the blocklist if needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --deep \
|
||||||
|
--blocklist "reboot,stow,def,q,Q,scan,kill"
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="caution" title="The scan command is dangerous">
|
||||||
|
On the Carryout G2, running `scan` in the ADC submenu without arguments on an uncalibrated AZ axis targets position 2147483647 (INT_MAX). The motor task blocks forever and the shell deadlocks. No serial input can recover it -- you need a hardware power cycle. Add `scan` to your blocklist if probing the ADC submenu.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### NVS submenu false positives
|
||||||
|
|
||||||
|
The NVS submenu treats any unrecognized input as a sequential index read (no error string). This means every candidate command "succeeds" during brute-force probing, producing false positives. The results are harmless (read-only) but noisy. The `--discover-only` mode avoids this issue since it only queries help.
|
||||||
|
|
||||||
|
### Using with non-Winegard devices
|
||||||
|
|
||||||
|
The tool is not Winegard-specific. It works with any serial console that has:
|
||||||
|
|
||||||
|
- A recognizable prompt string (auto-detected or specified with `--prompt`)
|
||||||
|
- A consistent error response for unknown commands (auto-detected or `--error`)
|
||||||
|
- A help command that lists available commands (configurable with `--help-cmd`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# U-Boot bootloader example
|
||||||
|
console-probe --port /dev/ttyUSB0 --baud 115200 \
|
||||||
|
--prompt "U-Boot>" --error "Unknown command" --help-cmd "help"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package structure
|
||||||
|
|
||||||
|
The `console-probe` package is organized as:
|
||||||
|
|
||||||
|
```
|
||||||
|
profile.py -- DeviceProfile + HelpEntry dataclasses
|
||||||
|
serial_io.py -- Prompt-aware serial I/O
|
||||||
|
discovery.py -- Auto-discovery, help parsing, submenu probing, candidate generation
|
||||||
|
report.py -- JSON report writer (format_version 2)
|
||||||
|
cli.py -- argparse CLI entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
Install and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
uv run console-probe --help
|
||||||
|
```
|
||||||
241
src/content/docs/guides/radio-telescope.mdx
Normal file
241
src/content/docs/guides/radio-telescope.mdx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
---
|
||||||
|
title: "Radio Telescope Mode"
|
||||||
|
description: Using the Carryout G2's built-in DVB tuner and azscanwxp command for RF sky mapping
|
||||||
|
sidebar:
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Carryout G2's firmware includes a built-in radio telescope mode: the `azscanwxp` command performs an azimuth sweep while cycling through DVB transponders at each position, measuring RSSI (received signal strength) at every grid point. Combined with the Ku-band LNB and motorized AZ/EL positioning, this turns the dish into a rudimentary RF imaging system.
|
||||||
|
|
||||||
|
This capability was originally discovered by Chris Davidson in his [winegard-sky-scan](https://github.com/cdavidson0522/winegard-sky-scan) project.
|
||||||
|
|
||||||
|
## Hardware requirements
|
||||||
|
|
||||||
|
- **Carryout G2** with firmware 02.02.48 (or compatible)
|
||||||
|
- RS-422 serial connection at 115200 baud (see [Cable Wiring](/guides/wiring/))
|
||||||
|
- Motors **homed and calibrated** (see [Calibration & Homing](/guides/calibration/))
|
||||||
|
- TV search disabled (see [Disabling TV Search](/guides/disabling-search/))
|
||||||
|
|
||||||
|
<Aside type="danger" title="Homed motors required">
|
||||||
|
The azscanwxp command requires homed motors. Running it on uncalibrated axes causes the firmware to target position 2147483647 (INT_MAX) -- the motor task blocks forever and the shell deadlocks. No serial input can recover it. You must power-cycle the dish to regain control. Always verify that position queries return valid angles (not INT_MAX) before scanning.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Setting up the DVB tuner
|
||||||
|
|
||||||
|
Before scanning, enable the LNA and configure the tuner frequency.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Enter the DVB submenu from root.**
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> dvb
|
||||||
|
DVB>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Enable the LNA.** The `lnbdc odu` command enables the LNB low-noise amplifier in outdoor unit mode, setting 13V (V-pol). The boot default is 18V (H-pol).
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> lnbdc odu
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check current channel parameters.**
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> dis
|
||||||
|
```
|
||||||
|
|
||||||
|
This shows the frequency, symbol rate, and LNB polarity currently configured.
|
||||||
|
|
||||||
|
4. **Select a transponder (optional).** If you want to scan at a specific frequency:
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> t 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `freqs` to list available transponder frequencies. For ham radio sky mapping, set the DVB tuner to a frequency near your target (e.g., 10 GHz Ku-band downconverted through the LNB to ~1178 MHz IF).
|
||||||
|
|
||||||
|
5. **Verify RSSI readings work.**
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> rssi 10
|
||||||
|
Reads:10 RSSI[avg: 498 cur: 502]
|
||||||
|
```
|
||||||
|
|
||||||
|
The noise floor is approximately 500. If you see readings near this value, the LNA is active but no strong signal is present -- this is normal for a clear sky pointing away from any satellites.
|
||||||
|
|
||||||
|
6. **Return to the root menu.**
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> q
|
||||||
|
TRK>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## The azscanwxp command
|
||||||
|
|
||||||
|
### Entering the command
|
||||||
|
|
||||||
|
The command lives in the MOT submenu:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> mot
|
||||||
|
MOT> azscanwxp [motor] [span] [resolution] [num_xponders]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Units | Description |
|
||||||
|
|-----------|------|-------|-------------|
|
||||||
|
| motor | int | -- | Motor ID (0=AZ, 1=EL) |
|
||||||
|
| span | float | degrees | Total azimuth sweep range |
|
||||||
|
| resolution | int | centidegrees (0.01 deg) | Step size per position |
|
||||||
|
| num_xponders | int | -- | Number of transponders to cycle at each position |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Sweep 10 degrees on azimuth at 1.00 degree steps, checking 3 transponders per position:
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> azscanwxp 0 10 100 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output format
|
||||||
|
|
||||||
|
Each measurement point produces a line:
|
||||||
|
|
||||||
|
```
|
||||||
|
Motor:<id> Angle:<cdeg> RSSI:<adc> Lock:<0/1> SNR:<dB> Scan Delta:<step>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `Motor` | Motor ID being swept |
|
||||||
|
| `Angle` | Current position in centidegrees |
|
||||||
|
| `RSSI` | Received signal strength (raw ADC count) |
|
||||||
|
| `Lock` | DVB carrier lock status (1 = locked, 0 = no lock) |
|
||||||
|
| `SNR` | Signal-to-noise ratio in dB |
|
||||||
|
| `Scan Delta` | Step count since last position |
|
||||||
|
|
||||||
|
## Sky mapping workflow
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Home the motors** if not already done.
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> mot
|
||||||
|
MOT> h 0
|
||||||
|
MOT> h 1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Position the dish at the starting elevation.** Choose an EL angle for the first scan strip.
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> a 1 30
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Enable the LNA** (if not done already).
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> q
|
||||||
|
TRK> dvb
|
||||||
|
DVB> lnbdc odu
|
||||||
|
DVB> q
|
||||||
|
TRK> mot
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the azimuth sweep.** Log the serial output to a file for post-processing.
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> azscanwxp 0 360 100 3
|
||||||
|
```
|
||||||
|
|
||||||
|
This sweeps the full 360-degree azimuth range at 1-degree steps with 3 transponders per position.
|
||||||
|
|
||||||
|
5. **Increment elevation and repeat.** Move up by your desired EL step and run another AZ sweep.
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> a 1 35
|
||||||
|
MOT> azscanwxp 0 360 100 3
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Post-process the data.** Parse the serial output into a grid of AZ/EL/RSSI values and render as a 2D heatmap. Each scan line gives you one row of the image.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## The azscan command (simpler variant)
|
||||||
|
|
||||||
|
The MOT submenu also has a simpler `azscan` command that doesn't cycle transponders:
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> azscan [az_range] [el_range] [delay]
|
||||||
|
```
|
||||||
|
|
||||||
|
This performs an AZ sweep with RSSI measurements at each position but without the transponder cycling. It's faster but provides less frequency diversity.
|
||||||
|
|
||||||
|
## RSSI and signal interpretation
|
||||||
|
|
||||||
|
| RSSI Value | Meaning |
|
||||||
|
|------------|---------|
|
||||||
|
| ~233-238 | ADC noise floor (no signal, no LNA) |
|
||||||
|
| ~489-502 | LNA active, noise floor (clear sky) |
|
||||||
|
| Above 600 | Weak signal detected |
|
||||||
|
| Above 1000 | Strong signal (likely a satellite) |
|
||||||
|
|
||||||
|
The ADC `rssi` command (in the ADC submenu) gives raw ADC counts. The DVB `rssi <n>` command (in the DVB submenu) averages over n samples and provides both average and current readings.
|
||||||
|
|
||||||
|
The PEAK submenu's `rssits` command alternates between H-pol (18V, even transponders) and V-pol (13V, odd transponders), reporting separate readings for each polarization:
|
||||||
|
|
||||||
|
```
|
||||||
|
PEAK> rssits
|
||||||
|
Even_sig = 489, Odd_sig = 235
|
||||||
|
```
|
||||||
|
|
||||||
|
V-pol (odd) has a quieter noise floor than H-pol (even).
|
||||||
|
|
||||||
|
## LNB polarity control via DiSEqC
|
||||||
|
|
||||||
|
The BCM4515 DVB tuner includes a DiSEqC 2.x controller accessible from the DVB submenu. DiSEqC (Digital Satellite Equipment Control) uses 22 kHz tone bursts on the coax LNB bias line to control LNB polarity and band selection.
|
||||||
|
|
||||||
|
For ham radio use, the key commands are:
|
||||||
|
|
||||||
|
| Command | Function |
|
||||||
|
|---------|----------|
|
||||||
|
| `lnbdc odu` | Set 13V (V-pol) -- enables LNA in outdoor unit mode |
|
||||||
|
| `send E0 10 38 F0` | Raw DiSEqC packet: select switch port 1 |
|
||||||
|
| `send E0 10 38 F1` | Raw DiSEqC packet: select switch port 2 |
|
||||||
|
|
||||||
|
The boot default is 18V (H-pol). Polarity affects which transponders are visible and the RSSI noise floor.
|
||||||
|
|
||||||
|
### DiSEqC timing parameters
|
||||||
|
|
||||||
|
| Parameter | Value | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| `ovraddr` | 0x11 | Target LNB address (standard first LNB) |
|
||||||
|
| `rrto` | 210 ms | Receive reply timeout |
|
||||||
|
| `pretx` | 15 ms | Pre-command TX delay |
|
||||||
|
| `tdthresh` | 110 | Tone detect threshold |
|
||||||
|
|
||||||
|
## Rotctld RSSI extensions
|
||||||
|
|
||||||
|
The `birdcage serve` command adds RSSI support to the rotctld protocol when running with a Carryout G2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Read RSSI (10 samples)
|
||||||
|
echo "R 10" | nc 127.0.0.1 4533
|
||||||
|
|
||||||
|
# Enable LNA
|
||||||
|
echo "L" | nc 127.0.0.1 4533
|
||||||
|
|
||||||
|
# Check capabilities
|
||||||
|
echo "D" | nc 127.0.0.1 4533
|
||||||
|
# Returns: CAPS:rssi,lna
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows external software to combine position control with signal measurements over a single TCP connection.
|
||||||
|
|
||||||
|
<LinkCard title="winegard-sky-scan" href="https://github.com/cdavidson0522/winegard-sky-scan" description="Chris Davidson's original Carryout G2 sky scan and rotator project on GitHub." />
|
||||||
220
src/content/docs/guides/satellite-tracking.mdx
Normal file
220
src/content/docs/guides/satellite-tracking.mdx
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
---
|
||||||
|
title: "Satellite Tracking with Gpredict"
|
||||||
|
description: End-to-end guide for tracking amateur radio satellites using Gpredict and the birdcage CLI
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This guide walks through the complete setup: installing the software, connecting to the dish, starting the rotctld server, configuring Gpredict, and tracking your first satellite pass.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before starting, make sure you have:
|
||||||
|
|
||||||
|
- A Winegard dish connected via serial (see [Cable Wiring](/guides/wiring/))
|
||||||
|
- The dish powered on and the TV search disabled (see [Disabling TV Search](/guides/disabling-search/))
|
||||||
|
- Motors homed and calibrated (see [Calibration & Homing](/guides/calibration/))
|
||||||
|
- The base aligned with "BACK" marking pointing to true North
|
||||||
|
- Clear sky view -- no obstructions taller than 8 inches within 32.5 inches of the base center
|
||||||
|
|
||||||
|
## Software installation
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Install the `birdcage` package.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the project directory
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
uv run birdcage --help
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Gpredict.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arch Linux
|
||||||
|
sudo pacman -S gpredict
|
||||||
|
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt install gpredict
|
||||||
|
|
||||||
|
# macOS (Homebrew)
|
||||||
|
brew install gpredict
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update TLE data in Gpredict.** On first launch, go to Edit > Update TLE Data from Network to fetch current orbital elements.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Architecture overview
|
||||||
|
|
||||||
|
The signal chain looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Gpredict ──TCP:4533──► rotctld server ──serial──► dish firmware
|
||||||
|
(tracking) (birdcage) (motor submenu)
|
||||||
|
```
|
||||||
|
|
||||||
|
Gpredict computes the satellite's predicted AZ/EL position and sends it to the rotctld server over TCP using the Hamlib rotctld protocol. The server translates those coordinates into motor commands and sends them over RS-485/RS-422 to the dish firmware. The firmware drives the stepper motors.
|
||||||
|
|
||||||
|
The `birdcage` package implements this full chain:
|
||||||
|
|
||||||
|
- **`protocol.py`** -- `FirmwareProtocol` ABC with implementations for HAL 2.05, HAL 0.0.00, and Carryout G2. Handles serial I/O, motor submenu navigation, position parsing.
|
||||||
|
- **`leapfrog.py`** -- Predictive overshoot algorithm to compensate for mechanical motor lag.
|
||||||
|
- **`antenna.py`** -- `BirdcageAntenna` class wrapping protocol + leapfrog. High-level `move_to()` and `get_position()`.
|
||||||
|
- **`rotctld.py`** -- `RotctldServer` TCP server implementing the `p`/`P`/`S`/`_`/`q` command set.
|
||||||
|
- **`cli.py`** -- Click CLI with `init`, `serve`, `pos`, and `move` subcommands.
|
||||||
|
|
||||||
|
## Starting the rotctld server
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Initialize the dish.** This waits for boot, kills the TV search, and enters the motor submenu.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HAL 2.05 (default)
|
||||||
|
uv run birdcage init --port /dev/ttyUSB0
|
||||||
|
|
||||||
|
# Carryout G2
|
||||||
|
uv run birdcage init --port /dev/ttyUSB0 --firmware g2
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for the "Antenna initialized and ready" message.
|
||||||
|
|
||||||
|
2. **Start the rotctld server.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HAL 2.05
|
||||||
|
uv run birdcage serve --port /dev/ttyUSB0
|
||||||
|
|
||||||
|
# Carryout G2
|
||||||
|
uv run birdcage serve --port /dev/ttyUSB0 --firmware g2
|
||||||
|
|
||||||
|
# If the dish is already initialized (skip boot wait)
|
||||||
|
uv run birdcage serve --port /dev/ttyUSB0 --firmware g2 --skip-init
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
|
||||||
|
```
|
||||||
|
rotctld server listening on 127.0.0.1:4533
|
||||||
|
Ctrl-C to stop
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify with a manual test.** In another terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query position
|
||||||
|
echo "p" | nc 127.0.0.1 4533
|
||||||
|
|
||||||
|
# Move to AZ=180, EL=45
|
||||||
|
echo "P 180.0 45.0" | nc 127.0.0.1 4533
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### CLI options
|
||||||
|
|
||||||
|
The `serve` command accepts these options:
|
||||||
|
|
||||||
|
| Option | Env Var | Default | Description |
|
||||||
|
|--------|---------|---------|-------------|
|
||||||
|
| `--port` | `BIRDCAGE_PORT` | `/dev/ttyUSB0` | Serial port for the adapter |
|
||||||
|
| `--firmware` | `BIRDCAGE_FIRMWARE` | `hal205` | Firmware variant (`hal205`, `hal000`, `g2`) |
|
||||||
|
| `--host` | `BIRDCAGE_LISTEN_HOST` | `127.0.0.1` | TCP listen address |
|
||||||
|
| `--listen-port` | `BIRDCAGE_LISTEN_PORT` | `4533` | TCP listen port |
|
||||||
|
| `--skip-init` | -- | false | Skip boot wait and search kill |
|
||||||
|
|
||||||
|
## Configuring Gpredict
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Open Gpredict** and go to **Edit > Preferences > Interfaces > Rotators**.
|
||||||
|
|
||||||
|
2. **Add a new rotator** with these settings:
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Name | Winegard Trav'ler |
|
||||||
|
| Host | 127.0.0.1 |
|
||||||
|
| Port | 4533 |
|
||||||
|
| Az type | 0 -> 180 -> 360 |
|
||||||
|
| Min El | 15 (or 18 for G2) |
|
||||||
|
| Max El | 90 (or 65 for G2) |
|
||||||
|
|
||||||
|
3. **Open the rotor control panel.** In the main Gpredict window, click the arrow in the bottom-right of a module and select **Antenna Control**.
|
||||||
|
|
||||||
|
4. **Select your rotator** from the dropdown in the antenna control panel.
|
||||||
|
|
||||||
|
5. **Select a satellite** to track from the satellite dropdown.
|
||||||
|
|
||||||
|
6. **Click "Track"** to begin automated tracking. Gpredict will send position updates to the rotctld server as the satellite moves across the sky.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="tip" title="Azimuth mode matters">
|
||||||
|
The "0 -> 180 -> 360" azimuth mode is required. This tells Gpredict that the rotator measures azimuth as 0-360 degrees (North through East). The alternative "180 -> 360 -> 180" mode is for rotators that use a different reference. Using the wrong mode will cause the dish to point in the wrong direction.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## The leapfrog algorithm
|
||||||
|
|
||||||
|
Satellite tracking requires continuous correction -- by the time the motors finish moving to a commanded position, a fast-moving LEO satellite has already moved. The leapfrog algorithm compensates by overshooting slightly in the direction of travel.
|
||||||
|
|
||||||
|
For each axis, if the difference between the target and current position exceeds a threshold, the target is nudged further:
|
||||||
|
|
||||||
|
| Delta (degrees) | Overshoot applied |
|
||||||
|
|-----------------|-------------------|
|
||||||
|
| > 2.0 | +/- 1.0 degree |
|
||||||
|
| > 1.0 | +/- 0.5 degree |
|
||||||
|
| ≤ 1.0 | None |
|
||||||
|
|
||||||
|
The direction of overshoot matches the direction of travel. This keeps the dish slightly *ahead* of the satellite, reducing tracking error during a pass.
|
||||||
|
|
||||||
|
The leapfrog algorithm is enabled by default. To disable it for a manual move:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run birdcage move --port /dev/ttyUSB0 --az 180 --el 45 --no-leapfrog
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="tip" title="Bug fix from upstream">
|
||||||
|
The original `birdcage.py` from Gabe's repo had a copy-paste bug where the elevation delta adjustments modified `target_az` instead of `target_el`. This is fixed in the `leapfrog.py` module. The same bug exists in the Trav'ler Pro repo.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Physical setup checklist
|
||||||
|
|
||||||
|
For accurate tracking, verify these physical conditions:
|
||||||
|
|
||||||
|
- **North alignment.** The base marking "BACK" should point to true North (not magnetic north). Use a compass and apply your local magnetic declination.
|
||||||
|
- **Level mounting.** The base should be level. A tilted base shifts all AZ/EL readings.
|
||||||
|
- **Clear horizon.** No obstructions taller than 8 inches within 32.5 inches of the base center. The dish arm sweeps a 32.5-inch radius.
|
||||||
|
- **Power.** Stable 120VAC to the RP-SK87 supply. Voltage dips during motor moves can cause missed steps.
|
||||||
|
|
||||||
|
## Quick reference: manual commands
|
||||||
|
|
||||||
|
While the rotctld server handles everything automatically, you can also control the dish directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query current position
|
||||||
|
uv run birdcage pos --port /dev/ttyUSB0 --firmware g2
|
||||||
|
|
||||||
|
# Move to a specific AZ/EL
|
||||||
|
uv run birdcage move --port /dev/ttyUSB0 --firmware g2 --az 180 --el 45
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rotctld protocol extensions (G2 only)
|
||||||
|
|
||||||
|
When running with a Carryout G2, the rotctld server supports additional commands beyond the standard Hamlib protocol:
|
||||||
|
|
||||||
|
| Command | Function |
|
||||||
|
|---------|----------|
|
||||||
|
| `R [n]` | Read RSSI signal strength (averaged over n samples) |
|
||||||
|
| `L` | Enable LNA for signal reception |
|
||||||
|
| `D` | Discover supported protocol extensions |
|
||||||
|
|
||||||
|
These commands allow integration with sky-scanning workflows. A non-G2 rotator returns `RPRT -6` (not available) for these commands.
|
||||||
|
|
||||||
|
<LinkCard title="Radio Telescope Mode" href="/guides/radio-telescope/" description="Use the G2's built-in DVB tuner for RF sky mapping with the azscanwxp command." />
|
||||||
145
src/content/docs/guides/wiring.mdx
Normal file
145
src/content/docs/guides/wiring.mdx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
title: "Cable Wiring"
|
||||||
|
description: How to wire the serial connection between your computer and each Winegard dish variant
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Winegard dish variants use two different serial bus standards. The Trav'ler family uses **RS-485 half-duplex** (2-wire), while the Carryout G2 uses **RS-422 full-duplex** (4-wire). Choosing the right adapter and wiring it correctly is the first step.
|
||||||
|
|
||||||
|
## RS-485 vs RS-422
|
||||||
|
|
||||||
|
| Property | RS-485 half-duplex | RS-422 / RS-485 full-duplex |
|
||||||
|
|----------|-------------------|----------------------------|
|
||||||
|
| Signal wires | 2 (+ GND) | 4 (+ GND) |
|
||||||
|
| Direction | One direction at a time | Both directions simultaneously |
|
||||||
|
| Max nodes | 32 drivers + 32 receivers | 1 driver + 10 receivers (RS-422) |
|
||||||
|
| Max distance | 1200m / 4000ft | 1200m / 4000ft |
|
||||||
|
| Max baud | ~10 Mbps | ~10 Mbps |
|
||||||
|
| Voltage swing | ±1.5V to ±5V differential | ±2V to ±5V differential |
|
||||||
|
| Bus turnaround | Required (adds latency) | Not needed |
|
||||||
|
| Typical adapter | USB-to-RS485 (DTECH, etc.) | USB-to-RS422 (FTDI, DIYables, etc.) |
|
||||||
|
|
||||||
|
The Trav'ler's RJ-25 connector exposes **both** a half-duplex pair (pins 2-3, labeled T/R) **and** a dedicated receive pair (pins 4-5, labeled RXD). Gabe's code uses only the half-duplex pair. Davidson's G2 code uses all four wires as RS-422. The same physical connector may support both modes depending on the firmware -- this is unconfirmed on the Trav'ler but worth testing if you have a 4-wire adapter.
|
||||||
|
|
||||||
|
## Adapter chain by variant
|
||||||
|
|
||||||
|
| Variant | Adapter | Wires Used |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| Trav'ler (Gabe's setup) | USB to RS232 to RS485 (DTECH) | Pins 2-3 only (half-duplex) |
|
||||||
|
| Carryout G2 (Davidson) | USB to RS422 (5V TTL) | Pins 2-5 (full-duplex) |
|
||||||
|
| Carryout G2 (confirmed) | DSD TECH SH-U11 USB to RS422 (FTDI FT232R) | Pins 1-5 (full-duplex + GND) |
|
||||||
|
| Carryout G2 (ESP32) | ESP32 UART2 to RS422 module (DIYables) | Pins 2-5 (full-duplex) |
|
||||||
|
|
||||||
|
## Wiring instructions
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="RS-485 (Trav'ler)">
|
||||||
|
|
||||||
|
The Trav'ler, Trav'ler HAL 2.05, and original Carryout use RS-485 half-duplex at **57600 baud**. The physical connector is an RJ-25 (6P6C).
|
||||||
|
|
||||||
|
### RJ-25 Pinout (bottom view, clip up)
|
||||||
|
|
||||||
|
| Pin | Label | RS-485 use |
|
||||||
|
|-----|-------|-----------|
|
||||||
|
| 1 | GND | Ground |
|
||||||
|
| 2 | T/R- | Shared data- |
|
||||||
|
| 3 | T/R+ | Shared data+ |
|
||||||
|
| 4 | RXD- | (unused in half-duplex) |
|
||||||
|
| 5 | RXD+ | (unused in half-duplex) |
|
||||||
|
| 6 | N/C | Not connected |
|
||||||
|
|
||||||
|
### Connection steps
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Get a USB-to-RS485 adapter.** The DTECH USB-to-RS232-to-RS485 chain is what Gabe used. A direct USB-to-RS485 adapter also works.
|
||||||
|
|
||||||
|
2. **Connect the shared data pair.** Wire the RS-485 adapter's A/+ terminal to pin 3 (T/R+) and B/- terminal to pin 2 (T/R-) on the RJ-25 connector.
|
||||||
|
|
||||||
|
3. **Connect ground.** Wire the adapter's GND terminal to pin 1 (GND).
|
||||||
|
|
||||||
|
4. **Leave pins 4-6 unconnected.** In half-duplex mode, the RXD pair and pin 6 are not used.
|
||||||
|
|
||||||
|
5. **Verify the serial port appears.** On Linux, look for `/dev/ttyUSB0` or similar. Check with `ls /dev/ttyUSB*`.
|
||||||
|
|
||||||
|
6. **Test with a terminal emulator.** Open the port at 57600 baud, 8N1. Press Enter -- you should see a `>` prompt or boot messages.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="caution" title="The Trav'ler Pro is different">
|
||||||
|
The Trav'ler Pro uses a **USB A-to-A cable** and shows up as `ttyACM0`, not `ttyUSB`. It does not use RS-485 at all. Connect it directly via USB and use 57600 baud.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="RS-422 (Carryout G2)">
|
||||||
|
|
||||||
|
The Carryout G2 uses RS-422 full-duplex at **115200 baud**. The physical connector is an RJ-12 (6P6C) -- same form factor as the RJ-25, same 6-pin modular jack.
|
||||||
|
|
||||||
|
### RJ-12 Pinout (clip away)
|
||||||
|
|
||||||
|
| Pin | Wire Color (Davidson) | Wire Color (confirmed) | RS-422 Function |
|
||||||
|
|-----|----------------------|----------------------|-----------------|
|
||||||
|
| 1 | White | Orange/White | GND (PE) |
|
||||||
|
| 2 | Red | Orange | TX+ (TA) -- computer to dish |
|
||||||
|
| 3 | Black | Green/White | TX- (TB) -- computer to dish |
|
||||||
|
| 4 | Yellow | Blue | RX+ (RA) -- dish to computer |
|
||||||
|
| 5 | Green | Blue/White | RX- (RB) -- dish to computer |
|
||||||
|
| 6 | Blue | Green | Not connected |
|
||||||
|
|
||||||
|
<Aside type="caution" title="Wire colors vary">
|
||||||
|
Wire colors differ between cable manufacturers. The "confirmed" column is from a standard 6P6C flat cable tested 2026-02-12. **Always verify with a multimeter before connecting.**
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Connection steps
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Get a USB-to-RS422 adapter.** The DSD TECH SH-U11 (FTDI FT232R) is confirmed working. Any RS-422 or 4-wire RS-485 adapter should work.
|
||||||
|
|
||||||
|
2. **Connect the TX pair (computer to dish).** Wire the adapter's TX+/TA terminal to pin 2 (TX+) and TX-/TB terminal to pin 3 (TX-).
|
||||||
|
|
||||||
|
3. **Connect the RX pair (dish to computer).** Wire the adapter's RX+/RA terminal to pin 4 (RX+) and RX-/RB terminal to pin 5 (RX-).
|
||||||
|
|
||||||
|
4. **Connect ground.** Wire the adapter's GND to pin 1 (GND).
|
||||||
|
|
||||||
|
5. **Verify the serial port appears.** On Linux, look for `/dev/ttyUSB0` or similar.
|
||||||
|
|
||||||
|
6. **Test with a terminal emulator.** Open the port at 115200 baud, 8N1. Press Enter -- you should see a `TRK>` prompt.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="caution" title="Polarity is critical">
|
||||||
|
Swapping +/- on the **RX pair** produces garbled data at the correct baud rate (systematic bit inversion, not random noise). Swapping +/- on the **TX pair** causes silent failure -- the dish doesn't respond because it can't decode the inverted framing. If you see garbled data, swap the +/- wires on the RX pair first.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## RS-422 module notes (DIYables MAX490)
|
||||||
|
|
||||||
|
If using the DIYables RS422-to-TTL module with an ESP32, be aware of the **failsafe concern**: the MAX490 does not have failsafe logic. When the RS-422 bus tri-states (no driver active), the receiver inputs float and may produce spurious bytes.
|
||||||
|
|
||||||
|
Workaround options:
|
||||||
|
|
||||||
|
1. **Add external bias resistors** -- pull A/RX+ toward V+ and B/RX- toward GND through ~560 ohm resistors. This biases the idle bus to a known logic-high state.
|
||||||
|
2. **Use prompt-terminated reads** -- our `CarryoutG2Protocol._send()` reads until `>` (ASCII 62) which naturally filters out garbage between commands.
|
||||||
|
3. **Keep cable runs short** -- under ~3m, the built-in 120 ohm termination is sufficient and bus float rarely causes issues.
|
||||||
|
|
||||||
|
## IDU/ODU cable wiring (if cut)
|
||||||
|
|
||||||
|
If you need to repair a cut cable between the indoor unit (IDU) and outdoor unit (ODU):
|
||||||
|
|
||||||
|
- **Top row:** Green, Yellow, Orange
|
||||||
|
- **Bottom row:** Red, Brown, Black
|
||||||
|
|
||||||
|
## Power
|
||||||
|
|
||||||
|
The dish runs on 120VAC input to the RP-SK87 power supply, which outputs 12VDC to the IDU. The internal coax carries 12-18VDC bias for the LNB.
|
||||||
|
|
||||||
|
<Aside type="caution" title="LNB bias voltage">
|
||||||
|
Do not connect 5V equipment (SDR LNAs, etc.) to the coax without bypassing the power injector. The 12-18VDC bias will damage equipment rated for lower voltages.
|
||||||
|
</Aside>
|
||||||
84
src/content/docs/index.mdx
Normal file
84
src/content/docs/index.mdx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: "Birdcage"
|
||||||
|
description: Satellite dish control for amateur radio tracking
|
||||||
|
template: splash
|
||||||
|
hero:
|
||||||
|
tagline: Repurposing motorized satellite TV dishes for amateur radio satellite tracking
|
||||||
|
image:
|
||||||
|
file: ../../assets/carryout-g2.jpg
|
||||||
|
alt: Winegard Carryout G2 satellite dish — the white dome radome that inspired the project name
|
||||||
|
actions:
|
||||||
|
- text: Get Started
|
||||||
|
link: /getting-started/
|
||||||
|
icon: right-arrow
|
||||||
|
- text: Console Commands
|
||||||
|
link: /reference/console-commands/
|
||||||
|
variant: minimal
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Card, CardGrid, LinkCard, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
<Aside type="tip" title="Standing on the shoulders of saveitforparts">
|
||||||
|
This project builds directly on the open-source work of **Gabe Emerson (KL1FI)** and his
|
||||||
|
[saveitforparts](https://www.youtube.com/@saveitforparts) YouTube channel. Gabe documented
|
||||||
|
five different Winegard dish variants, wrote Python control scripts for each, and shared
|
||||||
|
everything on GitHub. Without his work, none of this would exist.
|
||||||
|
[Read the full story →](/journal/origins/)
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Why "Birdcage"?
|
||||||
|
|
||||||
|
The Carryout G2's white dome radome does look like a birdcage -- and in ham radio, satellites are called "birds." A birdcage catches birds from the sky.
|
||||||
|
|
||||||
|
It's also a nod to [saveitforparts](https://www.youtube.com/@saveitforparts). Gabe saves discarded RV satellite dishes "for parts." We took the parts and built a birdcage from them.
|
||||||
|
|
||||||
|
## What is this?
|
||||||
|
|
||||||
|
Winegard makes motorized satellite TV dishes -- the Trav'ler, Carryout, and their variants -- designed to automatically find DirecTV and DISH Network satellites from an RV rooftop. They have stepper motors for azimuth and elevation, firmware consoles accessible over RS-485 or RS-422, and enough mechanical range to track objects across most of the sky.
|
||||||
|
|
||||||
|
Birdcage repurposes that hardware for **amateur radio satellite tracking**: pointing a dish at LEO and GEO satellites using orbital prediction software like Gpredict, controlled via the Hamlib `rotctld` protocol.
|
||||||
|
|
||||||
|
Along the way, we've reverse-engineered the firmware console of the Carryout G2 (all 12 submenus, 100+ commands), mapped the K60 MCU's GPIO pins live over serial, built a BLE-to-RS422 wireless bridge, and discovered a built-in radio telescope mode.
|
||||||
|
|
||||||
|
## Where to start
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<Card title="Getting Started" icon="rocket">
|
||||||
|
What hardware you need, how to connect, and your first satellite track.
|
||||||
|
[Start here →](/getting-started/)
|
||||||
|
</Card>
|
||||||
|
<Card title="Guides" icon="open-book">
|
||||||
|
Task-oriented guides: wiring, calibration, search disabling, firmware probing, BLE bridge build.
|
||||||
|
[Browse guides →](/guides/wiring/)
|
||||||
|
</Card>
|
||||||
|
<Card title="Reference" icon="document">
|
||||||
|
Complete firmware command inventory, NVS settings, GPIO pin maps, hardware specs.
|
||||||
|
[See reference →](/reference/firmware-variants/)
|
||||||
|
</Card>
|
||||||
|
<Card title="Understanding" icon="puzzle">
|
||||||
|
Software architecture, hardware platform internals, reverse-engineering methodology.
|
||||||
|
[Deep dives →](/understanding/architecture/)
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
## Supported hardware
|
||||||
|
|
||||||
|
Five Winegard dish variants are documented, with varying levels of testing:
|
||||||
|
|
||||||
|
| Variant | Connection | Status |
|
||||||
|
|---------|-----------|--------|
|
||||||
|
| Trav'ler (HAL 0.0.00) | RS-485 / RJ-25 | Supported (Gabe's original) |
|
||||||
|
| Trav'ler (HAL 2.05) | RS-485 / RJ-25 | Supported (Gabe's original) |
|
||||||
|
| Trav'ler Pro | USB A-to-A | Partially supported |
|
||||||
|
| Carryout (2003) | RS-485 / RJ-25 | Supported (different protocol) |
|
||||||
|
| Carryout G2 | RS-422 / RJ-12 | **Fully reverse-engineered** |
|
||||||
|
|
||||||
|
The Carryout G2 has the most complete documentation — over 100 firmware commands mapped across 12 submenus, NVS settings dumped, GPIO pins identified, and motor control characterized.
|
||||||
|
|
||||||
|
<LinkCard title="Firmware Variant Comparison" href="/reference/firmware-variants/" description="Detailed comparison table of all five variants: baud rates, motor commands, position formats, and protocol differences." />
|
||||||
|
|
||||||
|
## Project journal
|
||||||
|
|
||||||
|
This isn't just a reference manual. The [project journal](/journal/) captures the story of how we got here — the debugging sessions, the discoveries, the moments where a `?` in the right submenu revealed a whole new capability. It's a living section that grows as the project evolves.
|
||||||
|
|
||||||
|
<LinkCard title="Read the journal" href="/journal/" description="Discovery stories, debugging sessions, and the ongoing narrative of this project." />
|
||||||
178
src/content/docs/journal/console-probe-evolution.mdx
Normal file
178
src/content/docs/journal/console-probe-evolution.mdx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
title: "Console Probe Evolution"
|
||||||
|
description: From a monolithic 1,044-line script to a proper two-package architecture
|
||||||
|
sidebar:
|
||||||
|
order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The console-probe tool started as a single Python file. By the time it stabilized, it was a proper package with five modules, a CLI with multiple operating modes, JSON reporting with versioned schemas, and a clean separation from the dish-specific `birdcage` package. This is the story of how it grew.
|
||||||
|
|
||||||
|
## The monolithic beginning
|
||||||
|
|
||||||
|
The first version did everything in one file: open the serial port, detect the prompt, send `?`, parse the help output, try a list of candidate command strings, record which ones produced responses, print results. About 1,044 lines.
|
||||||
|
|
||||||
|
It worked. For the Trav'ler's simple firmware (bare `>` prompt, handful of commands, no submenus worth probing), it was fine. You ran it, it brute-forced its way through a few hundred candidates, told you which ones the firmware recognized, and you were done in a few minutes.
|
||||||
|
|
||||||
|
Then the G2 happened.
|
||||||
|
|
||||||
|
## Why it had to change
|
||||||
|
|
||||||
|
The Carryout G2's firmware has 12 submenus, each with its own prompt (`TRK>`, `MOT>`, `DVB>`, etc.), its own help format (some paginated across `?` and `man`), and its own set of commands. The monolithic script's assumptions broke everywhere:
|
||||||
|
|
||||||
|
**Prompt detection.** The original code assumed a bare `>` prompt. The G2 has `TRK>`, `MOT>`, `NVS>`, and ten others. The [prompt termination bug](/journal/prompt-termination-bug/) was the most visible failure, but the prompt handling issue ran deeper — you needed to track *which* prompt you expected, which meant tracking your current position in the menu hierarchy.
|
||||||
|
|
||||||
|
**Help parsing.** The Trav'ler's help output is a flat list of commands. The G2's help uses multiple formats: `Enter <cmd> - description` for submenus, `cmd - description` for regular commands, paginated across multiple help commands. The DVB submenu alone has 38 commands split between `?` and `man`.
|
||||||
|
|
||||||
|
**Navigation.** On the Trav'ler, there's essentially one menu level. On the G2, you enter submenus with their name (`mot`, `dvb`, `nvs`) and exit with `q`. Getting lost — sending `q` at the root level — terminates the shell entirely and requires a power cycle. The probe tool needed to track where it was and never send `q` at the wrong level.
|
||||||
|
|
||||||
|
**Error detection.** Different firmware versions return different error messages for unrecognized commands. The G2 uses a specific error string that we detect at runtime by sending a garbage command (`__xyzzy_probe__`) and reading whatever the firmware says back. This has to happen before probing starts.
|
||||||
|
|
||||||
|
A single file trying to handle all of this becomes unreadable fast.
|
||||||
|
|
||||||
|
## The refactoring
|
||||||
|
|
||||||
|
The split fell along natural boundaries:
|
||||||
|
|
||||||
|
### profile.py — what we know about the device
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class HelpEntry:
|
||||||
|
name: str # command name (lowercase)
|
||||||
|
description: str = "" # help description text
|
||||||
|
params: str = "" # parameter syntax, e.g. "[<motor> [angle]]"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceProfile:
|
||||||
|
port: str = "/dev/ttyUSB0"
|
||||||
|
baud: int = 115200
|
||||||
|
root_prompt: str = "" # e.g. "TRK>"
|
||||||
|
prompts: list[str] = ... # all known prompts
|
||||||
|
error_string: str = "" # e.g. "Invalid command."
|
||||||
|
known_commands: set[str] = ...
|
||||||
|
submenus: list[str] = ...
|
||||||
|
exit_cmd: str = "q"
|
||||||
|
line_ending: str = "\r"
|
||||||
|
submenu_help: dict[str, list[HelpEntry]] = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`DeviceProfile` is the central data structure. It starts almost empty and accumulates knowledge as discovery progresses. The probe detects the prompt, populates `root_prompt` and `prompts`. Help parsing populates `known_commands`, `submenus`, and `submenu_help`. Error detection fills `error_string`. By the end of a full probe run, the profile is a complete description of the firmware's console interface.
|
||||||
|
|
||||||
|
`HelpEntry` captures structured command documentation — not just the name, but the parameter syntax and description from the help output. This matters for the JSON report, where we want to preserve the firmware's own documentation of its commands.
|
||||||
|
|
||||||
|
### serial_io.py — talking to the device
|
||||||
|
|
||||||
|
Serial I/O got its own module because getting it right was the hardest part. Two functions: `send_cmd()` (send a command, read until prompt or timeout) and `detect_prompt()` (send a bare line ending, extract the prompt string). Plus the `_is_prompt_terminated()` function that handles the [three-layer prompt detection](/journal/prompt-termination-bug/).
|
||||||
|
|
||||||
|
Keeping serial I/O separate means the discovery engine never touches `pyserial` directly. It passes a `serial.Serial` handle and a `DeviceProfile` to `send_cmd()` and gets back a string. If we ever need to support a different transport (TCP socket for remote serial servers, mock serial for testing), we only change one module.
|
||||||
|
|
||||||
|
### discovery.py — the engine
|
||||||
|
|
||||||
|
This is where the intelligence lives. Five major functions:
|
||||||
|
|
||||||
|
**`parse_help_output()`** — takes raw help text, returns `(commands, submenus)`. Handles three help formats: `Enter <cmd> - description` (G2 submenu entries), `cmd - description` (standard), and bare command names. Filters out parameter placeholders like `<command>` and `<value>` that look like commands but aren't — the `_PARAM_PLACEHOLDERS` set catches these:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_PARAM_PLACEHOLDERS: set[str] = {
|
||||||
|
"command", "commands", "parameter", "parameters",
|
||||||
|
"value", "values", "index", "name", "arg", "args",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This set exists because of a real false positive: the firmware's `help [<command>]` usage text was being parsed as a command named `command`. The placeholder filter is a lesson learned.
|
||||||
|
|
||||||
|
**`parse_help_structured()`** — same parsing, but returns `HelpEntry` objects with descriptions and parameter syntax preserved. Used for the JSON report's `help_commands` section.
|
||||||
|
|
||||||
|
**`discover_submenu_help()`** — enters a submenu, queries `?`, tries `man` for paginated help, merges results, deduplicates. The `man` fallback was added specifically for the DVB submenu, which splits its 38 commands across two help pages.
|
||||||
|
|
||||||
|
**`probe_commands()`** — the brute-force engine. Iterates through a candidate list, sends each one, checks if the response is different from the error string, records hits. Handles shell termination (if a command kills the session) and submenu escapes (if a command accidentally exits the current submenu).
|
||||||
|
|
||||||
|
**`generate_candidates()`** — builds the candidate list from three sources: single characters (a-z, A-Z, 0-9), generic embedded debug commands (a hardcoded list of ~150 common commands like `dump`, `flash`, `reboot`, `config`), and external wordlists. Deduplicates and applies the blocklist (commands we never want to send, like `reboot`, `stow`, `q`).
|
||||||
|
|
||||||
|
### report.py — structured output
|
||||||
|
|
||||||
|
The JSON report uses a versioned schema (currently `format_version: 2`). Version 2 added the `menus` section with per-submenu structured data:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"device": { "port": "/dev/ttyUSB2", "baud": 115200 },
|
||||||
|
"detected": {
|
||||||
|
"root_prompt": "TRK>",
|
||||||
|
"error_string": "...",
|
||||||
|
"known_commands": ["a3981", "adc", "dvb", "mot", "nvs", ...],
|
||||||
|
"submenus": ["a3981", "adc", "dipswitch", ...]
|
||||||
|
},
|
||||||
|
"menus": {
|
||||||
|
"MOT": {
|
||||||
|
"prompt": "MOT>",
|
||||||
|
"help_commands": [
|
||||||
|
{ "cmd": "a", "description": "show/move position", "params": "[<id> <deg>]" },
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"probe_hits": [...],
|
||||||
|
"undiscovered": [...],
|
||||||
|
"stats": { "help_count": 25, "probe_count": 31, "undiscovered_count": 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `undiscovered` field is the interesting part — it lists commands found by brute-force probing that don't appear in the help output. These are the hidden commands, the ones the firmware responds to but doesn't advertise. On the G2, there aren't many (the help is pretty complete), but on other embedded platforms this list is where the gold is.
|
||||||
|
|
||||||
|
### cli.py — modes of operation
|
||||||
|
|
||||||
|
The CLI evolved to support three distinct workflows:
|
||||||
|
|
||||||
|
**`--discover-only`** — fast scan. Auto-detect prompt and error string, query help in every submenu, build a complete command inventory without sending any brute-force probes. Takes about 30 seconds on the G2 (12 submenus, ~2 seconds each). This is the "safe" mode — it only sends `?`, `man`, and navigation commands.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --discover-only --json /tmp/discover.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**`--deep`** — exhaustive probe. After discovery, brute-force every submenu with the full candidate list. This takes 15-30 minutes depending on the candidate count and timeout settings. It finds commands that help doesn't mention.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --deep --wordlist scripts/wordlists/winegard.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**`--submenu <name>`** — targeted probe. Run brute-force on a single submenu only. Useful when you've found an interesting menu and want to dig deeper without probing everything.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --submenu mot
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The blocklist (`--blocklist reboot,stow,def,q,Q`) exists because some commands are destructive. `reboot` reboots the MCU (obvious). `stow` folds the dish flat (dangerous with modified feeds). `def` restores factory defaults in EEPROM/NVS submenus. `q` terminates the shell at root level. The probe tool will never send these unless you explicitly remove them from the blocklist.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Automated vs. interactive discovery
|
||||||
|
|
||||||
|
The probe tool finds commands that respond without arguments — that's its fundamental approach. Send a string, check if the response differs from the error message. If it does, it's a command.
|
||||||
|
|
||||||
|
But many firmware commands require arguments to produce useful output. `a 0 180.0` moves azimuth to 180 degrees, but `a` alone just prints the current position. The probe finds `a` but can't tell you about the argument format. And commands that *require* arguments — like `e <idx>` in the NVS submenu — produce error output that looks different from the unknown-command error but isn't a successful response either.
|
||||||
|
|
||||||
|
This is why the full command inventory in the CLAUDE.md documentation was built from *both* automated probing and interactive `?` exploration. The probe finds the commands. Interactive sessions discover the argument formats, the edge cases, and the behavior that only shows up when you use the commands as intended.
|
||||||
|
|
||||||
|
The probe's `--discover-only` mode bridges this gap somewhat — by parsing the help output's parameter syntax (`[<motor> [angle]]`, `<hex>`, etc.), it captures *what* the firmware says the arguments should be, even if it can't verify them. The `HelpEntry.params` field preserves this.
|
||||||
|
|
||||||
|
## The two-package split
|
||||||
|
|
||||||
|
The final architectural decision was separating `console-probe` from `birdcage` into two installable packages:
|
||||||
|
|
||||||
|
**`console-probe`** — generic embedded console scanner. Knows nothing about Winegard dishes, motor commands, or satellite tracking. It understands serial ports, prompts, help parsing, and brute-force command discovery. You could point it at any embedded system with a text-based debug console.
|
||||||
|
|
||||||
|
**`birdcage`** — Winegard-specific dish control. Protocol implementations (`HAL205Protocol`, `CarryoutG2Protocol`), the leapfrog algorithm, antenna abstraction, rotctld server, CLI. This package knows about motor IDs, position formats, NVS indices, and satellite search sequences.
|
||||||
|
|
||||||
|
The split happened because we realized the probe tool was useful beyond this project. Any embedded system with a serial debug console — industrial controllers, network equipment, IoT devices, automotive ECUs — has the same pattern: send a command, read a response, figure out what commands exist. The discovery logic, help parsing, and brute-force probing are generic. The Winegard-specific knowledge belongs in the control package, not the probe.
|
||||||
|
|
||||||
|
Both packages are installed together via `uv sync` from the same repository, and they share the same `pyproject.toml` workspace. But they have separate entry points (`console-probe` and `birdcage`), separate source trees (`src/console_probe/` and `src/birdcage/`), and no code dependencies between them.
|
||||||
|
|
||||||
|
## What the probe finds vs. what we know
|
||||||
|
|
||||||
|
Running `--discover-only` on the G2 finds about 100 commands across 12 submenus in 30 seconds. Running `--deep` with the Winegard wordlist finds a handful more — undiscovered commands that respond to probing but aren't in the help output. Interactive `?` exploration in each submenu confirmed the full inventory: 6 in A3981, 5 in ADC, 1 in DIPSWITCH, 38 in DVB, 3 in EEPROM, 4 in GPIO, 1 in LATLON, 25 in MOT, 5 in NVS, 3 in OS, 6 in PEAK, 7 in STEP.
|
||||||
|
|
||||||
|
The gap between "what the probe finds" and "what exists" is small on the G2 — the firmware's help is reasonably complete. On other embedded platforms, that gap can be enormous. The probe is designed for the worst case: minimal help, undocumented commands, no error messages. The Winegard G2 just happens to be well-documented by its own firmware.
|
||||||
147
src/content/docs/journal/g2-discovery.mdx
Normal file
147
src/content/docs/journal/g2-discovery.mdx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
title: "Finding the G2 Protocol"
|
||||||
|
description: Connecting to the Carryout G2 for the first time and discovering it speaks a different dialect
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
We had the Trav'ler protocol mostly figured out. RS-485, 57600 baud, bare `>` prompt, `a <motor_id> <degrees>` to move, `AZ = / EL =` to read position. Clean, predictable, a little primitive. Then the Carryout G2 showed up.
|
||||||
|
|
||||||
|
## A different cable, a different bus
|
||||||
|
|
||||||
|
The first sign this wasn't going to be a drop-in was the connector. The Trav'ler uses an RJ-25 (6P6C) for RS-485 half-duplex — two wires carry both directions, one device talks at a time. The G2 has the same physical jack (RJ-12 6P6C), but it's RS-422 full-duplex: separate TX and RX differential pairs, four wires, simultaneous bidirectional communication.
|
||||||
|
|
||||||
|
Chris Davidson's [winegard-sky-scan](https://github.com/cdavidson0522/winegard-sky-scan) repo had the wiring guide. Without it, we'd have been guessing at a 6-pin connector with no silkscreen and no documentation.
|
||||||
|
|
||||||
|
| Pin | Wire Color (confirmed) | RS-422 Function |
|
||||||
|
|-----|----------------------|-----------------|
|
||||||
|
| 1 | Orange/White | GND (PE) |
|
||||||
|
| 2 | Orange | TX+ (TA) — computer to dish |
|
||||||
|
| 3 | Green/White | TX- (TB) — computer to dish |
|
||||||
|
| 4 | Blue | RX+ (RA) — dish to computer |
|
||||||
|
| 5 | Blue/White | RX- (RB) — dish to computer |
|
||||||
|
| 6 | Green | Not connected |
|
||||||
|
|
||||||
|
We used a DSD TECH SH-U11 USB-to-RS422 adapter with an FTDI FT232R chip. Plugged it in, opened a terminal at 57600 (the Trav'ler baud rate), and got nothing.
|
||||||
|
|
||||||
|
## The baud rate wasn't what we expected
|
||||||
|
|
||||||
|
57600 — silence. No prompt, no garbage, nothing. The adapter's RX LED wasn't even flickering during boot. Tried 9600, 19200, 38400 — nothing. Then 115200, and the terminal filled with text.
|
||||||
|
|
||||||
|
```
|
||||||
|
Bootloader v1.01
|
||||||
|
SPI1 init @ 4 MHz
|
||||||
|
Motor init: System=12Inch, master=40000 steps, slave=24960 steps
|
||||||
|
SPI2 init @ 6.857 MHz
|
||||||
|
EXTENDED_DVB_DEBUG ENABLED
|
||||||
|
BCM4515 ID 0x4515 Rev B0, FW v113.37
|
||||||
|
Enabled LNB STB
|
||||||
|
Ant ID - 12-IN G2
|
||||||
|
```
|
||||||
|
|
||||||
|
115200. Double the Trav'ler's baud rate. And a boot log that was actually telling us what it was doing — SPI bus initialization, motor configuration, DVB tuner identification. The Trav'ler just silently boots and eventually spits out `NoGPS` or `No LNB Voltage`.
|
||||||
|
|
||||||
|
## The polarity puzzle
|
||||||
|
|
||||||
|
Except those first few sessions weren't clean. Sometimes we'd get perfect ASCII output. Other times: garbled characters at the correct baud rate. Not random noise — structured garbage, with consistent timing but wrong byte values.
|
||||||
|
|
||||||
|
This is the signature of an inverted differential pair. RS-422/485 uses differential signaling: the logic level is determined by *which* wire is more positive. Swap the `+` and `-` wires on either the TX or RX pair, and every bit inverts. The UART framing still looks valid (start bit, data bits, stop bit), so you get characters — just the wrong ones.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
**Polarity is critical on the G2.** Swapping +/- on the RX pair (dish-to-computer) produces garbled data at the correct baud rate — systematic bit inversion, not random noise. Swapping +/- on the TX pair (computer-to-dish) causes silent failure — the dish never responds because it can't decode the inverted framing.
|
||||||
|
|
||||||
|
If you're getting structured garbage at 115200 baud, swap the two wires on your RX pair before trying anything else.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
Once we got the polarity sorted, the connection was rock solid. Full-duplex RS-422 at 115200 is noticeably snappier than half-duplex RS-485 at 57600 — no bus turnaround penalty.
|
||||||
|
|
||||||
|
## The first `?`
|
||||||
|
|
||||||
|
We sent a `?` and got back something we didn't expect:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK>?
|
||||||
|
Enter <a3981> - A3981 Stepper Driver IC
|
||||||
|
Enter <adc> - Analog to Digital Converter
|
||||||
|
Enter <dipswitch> - DIP Switch
|
||||||
|
Enter <dvb> - DVB Tuner
|
||||||
|
Enter <eeprom> - EEPROM
|
||||||
|
Enter <gpio> - GPIO
|
||||||
|
Enter <latlon> - Lat/Lon Calculator
|
||||||
|
Enter <mot> - Motor Control
|
||||||
|
Enter <nvs> - Non-Volatile Storage
|
||||||
|
Enter <os> - Operating System
|
||||||
|
Enter <peak> - Peak/DiSEqC Switch
|
||||||
|
Enter <step> - Stepper Motor
|
||||||
|
TRK>
|
||||||
|
```
|
||||||
|
|
||||||
|
Twelve submenus. The Trav'ler's HAL 2.05 firmware has maybe four or five commands at the root level. This thing had dedicated submenus for the motor driver ICs, the DVB tuner, GPIO pins, non-volatile storage, ADC readings — it was a full embedded debug shell.
|
||||||
|
|
||||||
|
And the prompt: `TRK>`. Not a bare `>`, but a labeled prompt that changes with context: `MOT>` in the motor submenu, `NVS>` in storage, `DVB>` in the tuner, `EE>` in EEPROM. The firmware is telling you where you are. The Trav'ler variants just give you `>` everywhere and you have to track context yourself.
|
||||||
|
|
||||||
|
## Position format — `Angle[0]`, not `AZ =`
|
||||||
|
|
||||||
|
The motor submenu confirmed the first real protocol difference. On the Trav'ler:
|
||||||
|
|
||||||
|
```
|
||||||
|
> a
|
||||||
|
AZ = 180.00 EL = 45.00 SK = 0.00
|
||||||
|
```
|
||||||
|
|
||||||
|
On the G2:
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> a
|
||||||
|
Angle[0] = 180.00
|
||||||
|
Angle[1] = 45.00
|
||||||
|
```
|
||||||
|
|
||||||
|
Same `a` command, different response format. No skew motor (the G2 doesn't have one). And `Angle[0]`/`Angle[1]` instead of named `AZ`/`EL` fields — the firmware treats motors as an indexed array rather than named axes. Move commands still use `a <id> <deg>`, which was a relief. But every regex we'd written for position parsing was wrong for this variant.
|
||||||
|
|
||||||
|
The move confirmation format is also different: `Angle = 46.00` (no array index) instead of `AZ = 46.00`. Just enough variation to break naive parsing.
|
||||||
|
|
||||||
|
## Killing the satellite search
|
||||||
|
|
||||||
|
On first boot, the G2 does the same annoying thing the Trav'ler does: it starts hunting for TV satellites. On the Trav'ler, you enter the search submenu and kill it. On the G2, the approach is different — you go nuclear.
|
||||||
|
|
||||||
|
NVS index 20: **Disable Tracker Proc**. Set it to TRUE and the firmware skips the satellite search entirely on every boot. Permanent, survives power cycles, stored in flash.
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> nvs
|
||||||
|
NVS> e 20 1
|
||||||
|
NVS> s
|
||||||
|
NVS>
|
||||||
|
```
|
||||||
|
|
||||||
|
`e 20 1` sets the value, `s` saves to flash. After this, the G2 boots to `TRK>` and waits. No search, no homing sequence (with NVS 20 = TRUE, the motors stay uncalibrated — the homing sequence is part of the tracker process).
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
With the tracker disabled, the azimuth position register reads as 2147483647 (INT_MAX) — a sentinel value meaning "uncalibrated." You need to explicitly home the motors with `h 0` and `h 1` in the MOT submenu before position readings are meaningful.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Firmware identification
|
||||||
|
|
||||||
|
The OS submenu gave us the full picture:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> os
|
||||||
|
OS> id
|
||||||
|
NVS Version: 02.02.48
|
||||||
|
System ID: 12-IN G2
|
||||||
|
Copyright 2013 - Winegard Company
|
||||||
|
```
|
||||||
|
|
||||||
|
Firmware 02.02.48, built in 2013. A 12-inch Carryout G2. The MCU is an NXP MK60DN512VLQ10 — a Kinetis K60, ARM Cortex-M4 at 96 MHz with 512 KB flash and 128 KB RAM. That's a serious microcontroller for a satellite dish from 2013. The original Trav'ler's MCU is unknown, and based on the simpler firmware shell, it's likely something much more modest.
|
||||||
|
|
||||||
|
## What we were looking at
|
||||||
|
|
||||||
|
By the end of that first session, the picture was clear: the Carryout G2 is a fundamentally more capable platform than the original Trav'ler. Two A3981 stepper motor driver ICs controlled via SPI. A BCM4515 DVB-S2 tuner with DiSEqC 2.x support. A full GPIO debug interface. Non-volatile storage with 130+ named parameters. An ADC subsystem for raw signal measurements.
|
||||||
|
|
||||||
|
And unlike the Trav'ler — where the IDU (indoor unit) is a dumb RS-485 passthrough to the ODU (outdoor unit) — the G2 is a single self-contained unit. Everything is on one board, accessible through one serial port. The debug shell is talking directly to the MCU that drives the motors, reads the tuner, and manages the entire system.
|
||||||
|
|
||||||
|
The motor control protocol (`a <id> <deg>`) was compatible enough with the Trav'ler family that writing a `CarryoutG2Protocol` subclass was straightforward. The hard part was everything else — the twelve submenus full of commands we'd never seen before, the different position format, the named prompts, the NVS configuration system. Mapping all of that would take the [console-probe](/journal/console-probe-evolution/) tool, the [GPIO mapping session](/journal/gpio-mapping-session/), and a lot of hours at the serial terminal.
|
||||||
|
|
||||||
|
But we knew it was worth it. This wasn't just a dish that could point at things. It had a built-in DVB-S2 receiver with RSSI readings, a signal analysis chain, and a firmware command called `azscanwxp` that we'd later discover was a [radio telescope mode](/journal/radio-telescope-mode/).
|
||||||
186
src/content/docs/journal/gpio-mapping-session.mdx
Normal file
186
src/content/docs/journal/gpio-mapping-session.mdx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
title: "GPIO Mapping Session"
|
||||||
|
description: Walking every GPIO pin on the K60 MCU live over serial to map the hardware peripherals
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Carryout G2's firmware has a `gpio` submenu. We didn't expect it to be particularly interesting — maybe a few pins for LEDs, maybe a relay output. Then we ran `gpio regs` and got back 92 pins worth of state data across five I/O ports.
|
||||||
|
|
||||||
|
That was the start of a long session cross-referencing live GPIO readings with the K60 datasheet pin mux table, boot log output, and the A3981 motor driver datasheet. By the end, we had a functional map of how the MCU talks to every major peripheral on the board.
|
||||||
|
|
||||||
|
## The dump
|
||||||
|
|
||||||
|
The GPIO submenu has four commands: `dir <pin>` (direction), `r <pin>` (read), `w <pin> <val>` (write), and `regs` (dump everything). The `regs` command is the one that matters:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> gpio
|
||||||
|
GPIO> regs
|
||||||
|
Port A:
|
||||||
|
A0=0 A1=0 A2=0 A3=0 A4=0 A5=1 A6=0 A7=0
|
||||||
|
A8=0 A9=0 A10=0 A11=0 A12=1 A13=0 A14=0 A15=0
|
||||||
|
A16=1 A17=1 A18=0 A19=0
|
||||||
|
A24=0 A25=0 A26=0 A27=0 A28=0 A29=0
|
||||||
|
Port B:
|
||||||
|
B0=1 B1=1 B2=1 B3=1 B4=0 B5=0 B6=0 B7=0
|
||||||
|
B8=0 B9=0 B10=0 B11=1
|
||||||
|
B16=0 B17=0 B18=0 B19=0
|
||||||
|
B20=0 B21=0 B22=0 B23=0
|
||||||
|
Port C:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Five ports (A through E), with each pin showing its current logic level. The first thing we noticed: A20-A23 and B12-B15 are absent. Not zero — absent. The MK60DN512VLQ10 in the 144-LQFP package doesn't bond those pins out. The firmware knows which pins exist on its own package variant.
|
||||||
|
|
||||||
|
Then there's `E29`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Port E:
|
||||||
|
...E28=1 Unknown bit E29
|
||||||
|
```
|
||||||
|
|
||||||
|
The firmware doesn't know what to do with this one. It's in the register space, but there's no pin mux entry for it. The K60 reference manual shows E29 doesn't exist on the 144-LQFP package — but the firmware still tries to read it and prints a confused message instead of silently skipping. A minor firmware bug, preserved in the GPIO register dump handler since 2013.
|
||||||
|
|
||||||
|
## Walking the pins
|
||||||
|
|
||||||
|
The `regs` dump gives you state but not direction. For that, you need `gpio dir <pin>` on each pin individually. We walked all 92:
|
||||||
|
|
||||||
|
```
|
||||||
|
GPIO> dir E0
|
||||||
|
E0: OUTPUT
|
||||||
|
GPIO> dir E1
|
||||||
|
E1: OUTPUT
|
||||||
|
GPIO> dir E2
|
||||||
|
E2: OUTPUT
|
||||||
|
GPIO> dir E3
|
||||||
|
E3: INPUT
|
||||||
|
GPIO> dir E4
|
||||||
|
E4: INPUT
|
||||||
|
GPIO> dir E5
|
||||||
|
E5: OUTPUT
|
||||||
|
```
|
||||||
|
|
||||||
|
Tedious, but necessary. The direction register tells you which pins the MCU is driving (OUTPUT) versus which it's listening to (INPUT). Combined with the K60 datasheet's pin mux table and the boot log, you can identify what each pin does.
|
||||||
|
|
||||||
|
<Aside type="note" title="Pin muxing on the K60">
|
||||||
|
The Kinetis K60 has configurable pin muxing — each GPIO pin can serve as a general-purpose I/O or be assigned to a peripheral function (SPI, UART, I2C, timer, etc.) via the Pin Control Register. When a pin is muxed to a peripheral, the GPIO direction register is *irrelevant* — the peripheral controls the pin directly. So pins that show "INPUT" in `gpio dir` may actually be SPI clocks or UART TX lines driven by hardware. The GPIO direction only applies when the pin is in GPIO mode (ALT1).
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## SPI1 — the motor drivers
|
||||||
|
|
||||||
|
The boot log gave us the first clue:
|
||||||
|
|
||||||
|
```
|
||||||
|
SPI1 init @ 4 MHz
|
||||||
|
Motor init: System=12Inch, master=40000 steps, slave=24960 steps, ratio=1.602564
|
||||||
|
```
|
||||||
|
|
||||||
|
SPI1 at 4 MHz, mode 0x03 (CPOL=1, CPHA=1). The K60 datasheet shows SPI1 on port E:
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | Alt Function | Live Dir | Live State | Assignment |
|
||||||
|
|---------|------|-------------|----------|------------|------------|
|
||||||
|
| PTE0 | E0 | SPI1_PCS1 | OUT | 1 | A3981 #2 chip select (EL motor) |
|
||||||
|
| PTE1 | E1 | SPI1_SOUT | OUT | 1 | MOSI — MCU to A3981 |
|
||||||
|
| PTE2 | E2 | SPI1_SCK | OUT | 1 | SPI clock |
|
||||||
|
| PTE3 | E3 | SPI1_SIN | IN | 0 | MISO — A3981 to MCU |
|
||||||
|
| PTE4 | E4 | SPI1_PCS0 | IN | 1 | A3981 #1 chip select (AZ motor) |
|
||||||
|
| PTE5 | E5 | SPI1_PCS2 | OUT | 1 | Possibly A3981 RESET or enable |
|
||||||
|
|
||||||
|
E4 shows INPUT in the GPIO direction register, but it's muxed to SPI1_PCS0 — the SPI controller manages chip select assertion directly, so the GPIO direction is meaningless here. The live state of 1 (high) on the chip select lines means both A3981s are deselected (active-low CS), which is the expected idle state.
|
||||||
|
|
||||||
|
The A3981 is an Allegro stepper motor driver. Two of them on SPI1 — one for azimuth (PCS0, 40000 steps/rev), one for elevation (PCS1, 24960 steps/rev). They support 1/16 microstepping in AUTO mode, which matches what the firmware's `a3981 ss` command reports.
|
||||||
|
|
||||||
|
We could confirm this from the `a3981` submenu:
|
||||||
|
|
||||||
|
```
|
||||||
|
A3981> cm
|
||||||
|
AZ Current Control Mode: AUTO
|
||||||
|
EL Current Control Mode: AUTO
|
||||||
|
A3981> sm
|
||||||
|
AZ Step Mode: AUTO
|
||||||
|
EL Step Mode: AUTO
|
||||||
|
A3981> diag
|
||||||
|
AZ DIAG: OK EL DIAG: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Both drivers healthy, both in AUTO mode. The DIAG pins on the A3981 are active-low open-drain — the "OK" reading means the GPIO pins reading the fault output are pulled high. The exact GPIO pins for DIAG are TBD (we haven't isolated them from the `regs` dump yet), but they're likely somewhere in the cluster of unidentified input pins.
|
||||||
|
|
||||||
|
## SPI2 — the DVB tuner
|
||||||
|
|
||||||
|
```
|
||||||
|
SPI2 init @ 6.857 MHz
|
||||||
|
BCM4515 ID 0x4515 Rev B0, FW v113.37
|
||||||
|
```
|
||||||
|
|
||||||
|
SPI2 at 6.857 MHz, same mode 0x03. On port D:
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | Alt Function | Live Dir | Live State | Assignment |
|
||||||
|
|---------|------|-------------|----------|------------|------------|
|
||||||
|
| PTD11 | D11 | SPI2_PCS0 | OUT | 1 | BCM4515 chip select |
|
||||||
|
| PTD12 | D12 | SPI2_SCK | IN | 1 | SPI clock |
|
||||||
|
| PTD13 | D13 | SPI2_SOUT | IN | 1 | MOSI — MCU to BCM4515 |
|
||||||
|
| PTD14 | D14 | SPI2_SIN | — | 0 | MISO — BCM4515 to MCU |
|
||||||
|
| PTD15 | D15 | SPI2_PCS1 | — | 0 | Secondary chip select (unused?) |
|
||||||
|
|
||||||
|
D12 and D13 show INPUT in the GPIO register despite being SPI clock and MOSI — again, the peripheral mux overrides GPIO direction. D15 is a secondary chip select that's held low; likely unused (the BCM4515 only needs one CS).
|
||||||
|
|
||||||
|
The BCM4515 is a Broadcom DVB-S2 demodulator. It handles satellite signal reception — carrier tracking, forward error correction, NID (Network ID) detection. The firmware talks to it over SPI at nearly 7 MHz, which is fast enough for real-time signal monitoring (RSSI, AGC, SNR readings).
|
||||||
|
|
||||||
|
## UART4 — the console we're talking through
|
||||||
|
|
||||||
|
```
|
||||||
|
GPIO> dir E24
|
||||||
|
E24: OUTPUT
|
||||||
|
GPIO> dir E25
|
||||||
|
E25: INPUT
|
||||||
|
GPIO> dir E26
|
||||||
|
E26: INPUT
|
||||||
|
```
|
||||||
|
|
||||||
|
Port E pins 24-28:
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | Alt Function | Live Dir | Live State | Notes |
|
||||||
|
|---------|------|-------------|----------|------------|-------|
|
||||||
|
| PTE24 | E24 | UART4_TX | OUT | 1 | Console TX (to computer RX pair) |
|
||||||
|
| PTE25 | E25 | UART4_RX | IN | 1 | Console RX (from computer TX pair) |
|
||||||
|
| PTE26 | E26 | UART4_CTS | IN | 1 | Hardware flow control (idle high) |
|
||||||
|
| PTE27 | E27 | GPIO? | IN | 1 | Unknown — RTS, or just pulled up |
|
||||||
|
| PTE28 | E28 | GPIO? | IN | 1 | Unknown |
|
||||||
|
|
||||||
|
This is the RS-422 console port — the one we're using to send all these queries. UART4_TX on E24 drives one differential pair of the RS-422 transceiver (which connects to pin 4/5 on the RJ-12, our RX pair). UART4_RX on E25 receives from the other pair (pin 2/3, our TX pair). CTS on E26 is idle high, meaning the firmware is ready to receive.
|
||||||
|
|
||||||
|
The K60 has five UART peripherals (UART0-4). UART4 is the last one, and it's the debug console. The firmware probably uses UART0 or UART1 for the DVB tuner's serial interface (some BCM4515 configurations use SPI + UART), but we haven't confirmed that yet.
|
||||||
|
|
||||||
|
## The mystery pins
|
||||||
|
|
||||||
|
After mapping the three major peripherals (SPI1, SPI2, UART4), we still had a bunch of pins in known states that we couldn't attribute to specific functions:
|
||||||
|
|
||||||
|
| GPIO | Dir | State | Best guess |
|
||||||
|
|------|-----|-------|-----------|
|
||||||
|
| D10 | OUT | 1 | BCM4515 reset or power enable — it's adjacent to the SPI2 cluster |
|
||||||
|
| B0-B3 | — | 1 | Contiguous high block — possibly SPI0 or I2C0, both available on port B |
|
||||||
|
| B11 | — | 1 | Isolated high pin — status LED or peripheral enable |
|
||||||
|
| C10-C13 | — | 1 | Four contiguous pins, all high — could be a bus interface or DIP switch reads |
|
||||||
|
| C18 | — | 1 | Single pin — LNB voltage control relay? The `lnbdc odu` command switches 13V/18V |
|
||||||
|
|
||||||
|
The DIP switch reads are particularly interesting. The `dipswitch` submenu returns `val:ffffff01` — a 32-bit register read. The `0xffffff01` pattern (bits 1-24 all high, bit 0 high) suggests GPIO pins with internal pullups and all switches in the OFF/up position. Port C has enough pins in the right state to be candidates, but without being able to flip individual switches during a GPIO read, we can't confirm the mapping.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
D10 being OUTPUT=1 adjacent to the SPI2 pins is the strongest candidate for BCM4515 reset or enable. The Broadcom datasheet (which we don't have — it's not public) would confirm this, but the boot sequence behavior is consistent: the MCU would assert reset, initialize SPI2, then release reset before talking to the tuner.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## What the map tells us
|
||||||
|
|
||||||
|
The functional pin map isn't just an academic exercise. It tells us what's possible:
|
||||||
|
|
||||||
|
**Motor control is SPI-based, not bit-banged.** The A3981 drivers are on a proper SPI bus with dedicated chip selects. The MCU can read back driver status (fault flags, step mode, current mode) in real time. This is why the `a3981 diag` command works — it's doing an SPI read of the DIAG register, not just checking a GPIO fault pin.
|
||||||
|
|
||||||
|
**The DVB tuner has a high-bandwidth connection.** SPI2 at 6.857 MHz means the MCU can stream signal data from the BCM4515 fast enough for real-time RSSI and AGC monitoring. The `dvb agc` streaming command works because the bus can sustain the data rate.
|
||||||
|
|
||||||
|
**The UART has hardware flow control available.** CTS is wired (E26). If we ever need to send large data blocks to the firmware (firmware updates, configuration dumps), we have flow control to prevent buffer overruns. Currently unused by our code since command/response at 115200 never overruns.
|
||||||
|
|
||||||
|
**There are unaccounted pins.** Port B0-B3, C10-C13, and several others are in definite states but unmapped. These could be additional peripherals (I2C EEPROM? second UART? temperature sensor?) or just board-level power control. A board-level reverse engineering session (tracing PCB traces under a microscope) would resolve these, but we'd need to open the dish enclosure for that.
|
||||||
|
|
||||||
|
The GPIO map is a snapshot — it captures the board's state at idle, after boot, with the tracker disabled. Different states during motor movement or DVB tuning would show different patterns (chip select toggling, SPI activity, motor driver current modes changing from LOW to HIGH torque). But as a static reference for "what's connected to what," it's the most detailed view we have without physical board access.
|
||||||
30
src/content/docs/journal/index.mdx
Normal file
30
src/content/docs/journal/index.mdx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
title: Project Journal
|
||||||
|
description: The ongoing story of reverse-engineering Winegard satellite dishes for amateur radio
|
||||||
|
sidebar:
|
||||||
|
order: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
import { LinkCard, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This section is different from the rest of the documentation. Where the guides and reference pages tell you *what* and *how*, the journal tells you *why* and *when* — the story of how this project came together.
|
||||||
|
|
||||||
|
Each entry captures a discovery session, a debugging breakthrough, or a design decision as it happened. They're written close to the moment, with the rough edges left in. Some entries document dead ends. Some document surprises. All of them are part of the record.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
This is a living section. New entries are added as the project evolves. The most recent entries reflect the current state of the project; earlier ones reflect what we knew at the time.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Entries
|
||||||
|
|
||||||
|
<LinkCard title="Origins — Standing on saveitforparts" href="/journal/origins/" description="How Gabe Emerson's open-source work made this project possible." />
|
||||||
|
|
||||||
|
<LinkCard title="Finding the G2 Protocol" href="/journal/g2-discovery/" description="Connecting to the Carryout G2 for the first time and discovering it speaks a different dialect." />
|
||||||
|
|
||||||
|
<LinkCard title="The Prompt Termination Bug" href="/journal/prompt-termination-bug/" description="How a one-character parsing bug in console-probe led to understanding the firmware's serial protocol." />
|
||||||
|
|
||||||
|
<LinkCard title="GPIO Mapping Session" href="/journal/gpio-mapping-session/" description="Walking every GPIO pin on the K60 MCU live over serial to map the hardware." />
|
||||||
|
|
||||||
|
<LinkCard title="Console Probe Evolution" href="/journal/console-probe-evolution/" description="From a 1,044-line monolithic script to a proper two-package architecture." />
|
||||||
|
|
||||||
|
<LinkCard title="Radio Telescope Mode" href="/journal/radio-telescope-mode/" description="Discovering azscanwxp and what a satellite TV dish can do as an RF imager." />
|
||||||
53
src/content/docs/journal/origins.mdx
Normal file
53
src/content/docs/journal/origins.mdx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
title: "Origins — Standing on saveitforparts"
|
||||||
|
description: How Gabe Emerson's open-source work made this project possible
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The idea of pointing a satellite TV dish at things other than TV satellites isn't new. But making it *practical* — with working code, documented protocols, and tested hardware — required someone to go first. That someone was **Gabe Emerson (KL1FI)**, who publishes as [saveitforparts](https://www.youtube.com/@saveitforparts) on YouTube.
|
||||||
|
|
||||||
|
## What Gabe did
|
||||||
|
|
||||||
|
Gabe got his hands on a Winegard Trav'ler — one of those big motorized dishes that sit on RV rooftops — and figured out how to control it. He documented the RS-485 serial protocol, wrote Python scripts to send motor commands, and integrated it with Gpredict via the Hamlib `rotctld` protocol. Then he published everything on GitHub.
|
||||||
|
|
||||||
|
Then he did it again. And again. Five times, across five different Winegard hardware variants:
|
||||||
|
|
||||||
|
| Repository | Variant | What it covers |
|
||||||
|
|-----------|---------|---------------|
|
||||||
|
| [Travler_Rotor](https://github.com/saveitforparts/Travler_Rotor) | HAL 0.0.00 | Original Trav'ler control script |
|
||||||
|
| [Trav-ler-Rotor-For-HAL-2.05](https://github.com/saveitforparts/Trav-ler-Rotor-For-HAL-2.05) | HAL 2.05.003 | Updated firmware variant |
|
||||||
|
| [Travler-Pro-Rotor](https://github.com/saveitforparts/Travler-Pro-Rotor) | Trav'ler Pro | USB A-to-A connection, ODU tunneling |
|
||||||
|
| [Carryout-Rotor](https://github.com/saveitforparts/Carryout-Rotor) | Carryout (2003) | Different protocol (`g` not `a`), stall-detect homing |
|
||||||
|
| [Carryout-Radio-Telescope](https://github.com/saveitforparts/Carryout-Radio-Telescope) | Carryout | RF scanning and sky imaging |
|
||||||
|
|
||||||
|
Each repo includes the complete wiring guide, the Python control script, and enough documentation to get someone else started. That's the part that matters — he didn't just make it work for himself, he made it reproducible.
|
||||||
|
|
||||||
|
## What we built on top
|
||||||
|
|
||||||
|
This project started with Gabe's HAL 2.05 code. The original `travler_rotor.py` — 275 lines of serial I/O, motor commands, and Gpredict integration — was the foundation.
|
||||||
|
|
||||||
|
We restructured it into a proper Python package (`birdcage`) with separate modules for protocol abstraction, the leapfrog overshoot algorithm, high-level antenna control, the rotctld server, and a CLI. Along the way we found and fixed the [elevation leapfrog bug](/understanding/known-bugs/) that was present in both the Trav'ler and Trav'ler Pro repos.
|
||||||
|
|
||||||
|
Then the Carryout G2 arrived. It spoke a different dialect — RS-422 instead of RS-485, 115200 baud instead of 57600, `Angle[0]` instead of `AZ =` — and we needed to reverse-engineer its firmware console from scratch. That work produced the [console-probe](/reference/console-probe-cli/) tool, the [full command inventory](/reference/console-commands/), and eventually the [GPIO pin map](/reference/gpio-pinmap/) of the K60 MCU.
|
||||||
|
|
||||||
|
Chris Davidson's [winegard-sky-scan](https://github.com/cdavidson0522/winegard-sky-scan) project provided the other critical piece — the RS-422 wiring guide for the Carryout G2 and the discovery of the `azscanwxp` [radio telescope mode](/guides/radio-telescope/).
|
||||||
|
|
||||||
|
## The videos
|
||||||
|
|
||||||
|
Gabe's YouTube demos are worth watching if you want to see these dishes in action:
|
||||||
|
|
||||||
|
- [Trav'ler v1 demo](https://youtu.be/X1hnReHepFI) — first proof-of-concept, manually controlled tracking
|
||||||
|
- [Trav'ler v2 demo](https://www.youtube.com/watch?v=URJZjo5EcpQ) — automated tracking with Gpredict integration
|
||||||
|
|
||||||
|
<Aside type="tip" title="Contact">
|
||||||
|
Gabe can be reached at gabe@saveitforparts.com. He's been generous with his time and knowledge throughout this project space.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Why this matters
|
||||||
|
|
||||||
|
Amateur radio satellite tracking requires a dish that can point at a specific spot in the sky and follow a moving object. Commercial amateur radio rotators cost $500-$2000+. A used Winegard Trav'ler or Carryout from an RV salvage yard costs $50-$200 and has more mechanical range than most purpose-built rotators.
|
||||||
|
|
||||||
|
The gap was software — translating between "azimuth 145.3°, elevation 32.7°" and the specific serial commands each Winegard variant expects. Gabe bridged that gap first. Everything here is an extension of that work.
|
||||||
178
src/content/docs/journal/prompt-termination-bug.mdx
Normal file
178
src/content/docs/journal/prompt-termination-bug.mdx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
title: "The Prompt Termination Bug"
|
||||||
|
description: How a one-character parsing bug in console-probe led to understanding the firmware's serial protocol
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Serial protocols are deceptively simple. Bytes go in, bytes come out. No framing beyond start/stop bits, no packet headers, no length fields. When you send a command to an embedded console, how do you know the response is done?
|
||||||
|
|
||||||
|
On the Winegard firmware, the answer looks obvious: the prompt character `>` appears at the end of every response. Read bytes until you see `>`, and you've got the full response. That was the original approach. It worked great until it didn't.
|
||||||
|
|
||||||
|
## The symptom
|
||||||
|
|
||||||
|
The console-probe tool was returning truncated responses. Not every time — just for certain commands and certain submenus. A help query that should return 15 lines of output would come back with 6. A command that should show motor positions would cut off mid-value. The truncation points seemed random.
|
||||||
|
|
||||||
|
The responses *always* ended with `>`. That was the tell — the reader was stopping exactly where it thought the prompt was. But the prompt wasn't there. Something else was.
|
||||||
|
|
||||||
|
## Where `>` hides
|
||||||
|
|
||||||
|
Consider the DVB submenu's help output. It includes lines like:
|
||||||
|
|
||||||
|
```
|
||||||
|
Enter <dvb> - DVB Tuner
|
||||||
|
```
|
||||||
|
|
||||||
|
That angle bracket `>` at the end of `<dvb>` is just part of the parameter syntax notation. But to a byte-level reader looking for `>` as a termination character, it's indistinguishable from a prompt.
|
||||||
|
|
||||||
|
Or NVS parameter descriptions:
|
||||||
|
|
||||||
|
```
|
||||||
|
help [<command>]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `>` in `<command>` is another false positive. And the firmware's own output sometimes includes `>` in comparison operators, hex dumps, and log messages. Every one of these is a trap for a naive "read until `>`" strategy.
|
||||||
|
|
||||||
|
The fundamental problem: **`>` is used as both a prompt character and a content character**, and at the byte level they look identical.
|
||||||
|
|
||||||
|
## How the original code worked
|
||||||
|
|
||||||
|
The first version of the serial reader was straightforward:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Original approach (simplified)
|
||||||
|
def read_response(ser):
|
||||||
|
buf = bytearray()
|
||||||
|
while True:
|
||||||
|
chunk = ser.read(4096)
|
||||||
|
buf.extend(chunk)
|
||||||
|
text = buf.decode('utf-8', errors='replace')
|
||||||
|
if text.rstrip().endswith('>'):
|
||||||
|
break
|
||||||
|
return text
|
||||||
|
```
|
||||||
|
|
||||||
|
Read chunks, accumulate them, check if the last non-whitespace character is `>`. If so, we're done. This works perfectly when:
|
||||||
|
|
||||||
|
1. The response contains no `>` characters except the final prompt
|
||||||
|
2. The prompt is always a bare `>`
|
||||||
|
|
||||||
|
On the original Trav'ler (HAL 0.0.00 and 2.05), both conditions are mostly true. The help output is sparse, the prompt is just `>`, and the firmware doesn't generate much output that contains angle brackets. So the bug hid.
|
||||||
|
|
||||||
|
## The G2 broke everything
|
||||||
|
|
||||||
|
The Carryout G2's firmware is far more verbose than the Trav'ler's. Twelve submenus, paginated help (the DVB submenu has a `?` page and a `man` page), parameter syntax with angle brackets everywhere, and named prompts like `TRK>`, `MOT>`, `DVB>`.
|
||||||
|
|
||||||
|
The named prompts actually made things both worse and better. Worse because `TRK>` contains `>` and so does every other prompt string. Better because `TRK>` is a much more specific pattern than bare `>`.
|
||||||
|
|
||||||
|
Here's what a real failure looked like. Sending `?` in the root menu:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK>?
|
||||||
|
Enter <a3981> - A3981 Stepper Driver IC
|
||||||
|
Enter <adc> - Analog to Digital Converter
|
||||||
|
```
|
||||||
|
|
||||||
|
The reader would stop at the `>` in `<a3981>` on the first help line. Two lines of output instead of twelve. The rest of the help text was still sitting in the serial buffer, and the next command would get a response that started with leftover help output — corrupting everything downstream.
|
||||||
|
|
||||||
|
## The fix: prompt-aware reading
|
||||||
|
|
||||||
|
The insight was that a real firmware prompt has specific structural properties that random `>` characters in content don't:
|
||||||
|
|
||||||
|
1. A prompt appears **at the start of a line** (or at least the end of the last line)
|
||||||
|
2. A prompt is a **specific string** — `TRK>`, `MOT>`, `NVS>`, not just `>`
|
||||||
|
3. A prompt is **never inside brackets** — `[<parameter>]` contains `>` but it's clearly parameter syntax
|
||||||
|
|
||||||
|
The fix in `serial_io.py` implements this as a multi-layer check:
|
||||||
|
|
||||||
|
```python
|
||||||
|
PROMPT_RE = re.compile(r"(\S+[>$#])\s*$")
|
||||||
|
|
||||||
|
def _is_prompt_terminated(text: str, profile: DeviceProfile) -> bool:
|
||||||
|
stripped = text.rstrip()
|
||||||
|
if not stripped:
|
||||||
|
return False
|
||||||
|
|
||||||
|
last_line = stripped.split("\n")[-1]
|
||||||
|
|
||||||
|
if profile.prompts:
|
||||||
|
# Check known prompts first (fast path)
|
||||||
|
last_stripped = last_line.rstrip()
|
||||||
|
for p in profile.prompts:
|
||||||
|
if last_stripped.endswith(p):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Accept a PROMPT_RE match only if no brackets on that line
|
||||||
|
if "[" not in last_line:
|
||||||
|
m = PROMPT_RE.search(last_line)
|
||||||
|
if m:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# No known prompts yet — fallback to bare > check
|
||||||
|
return stripped.endswith(">")
|
||||||
|
```
|
||||||
|
|
||||||
|
Three layers of defense:
|
||||||
|
|
||||||
|
**Layer 1: Known prompt matching.** Once we've discovered the device's prompts (during the auto-discovery phase), we check the last line against the exact known prompt strings. `TRK>` matches `TRK>`. `Enter <a3981>` does not match `TRK>`. Fast, precise, no false positives.
|
||||||
|
|
||||||
|
**Layer 2: Pattern matching with bracket exclusion.** For cases where we might encounter a new prompt we haven't seen (entering an undiscovered submenu), the regex `(\S+[>$#])\s*$` matches prompt-like strings at the end of a line. But only if the line doesn't contain `[` — which filters out parameter syntax like `help [<command>]`.
|
||||||
|
|
||||||
|
**Layer 3: Bare `>` fallback.** During the very first connection, before we've discovered any prompt strings, we fall back to the original `endswith('>')` check. This is the least reliable layer, but it only runs during initial probe when we need *some* heuristic to get started.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The fallback is unavoidable — you need to bootstrap somehow. The key is that it only runs during `detect_prompt()`, which sends a bare carriage return and reads the response. At that point, the firmware's response is just the prompt itself (no content), so the bare `>` check works. Once we have the prompt string, layer 1 takes over.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## The bootstrap problem
|
||||||
|
|
||||||
|
There's a chicken-and-egg situation here. To read responses correctly, you need to know the prompt. To discover the prompt, you need to read a response. The `detect_prompt()` function handles this by sending a bare line ending (carriage return) and reading whatever comes back:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def detect_prompt(ser, profile):
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
A bare CR on the Winegard firmware produces just the prompt — `TRK>` or whatever menu you're in. No content, no help text, no `>` characters hiding in angle brackets. So the simple `endswith('>')` check is safe here. The regex then extracts the full prompt string (`TRK>`, not just `>`), which goes into `profile.prompts` for all subsequent reads.
|
||||||
|
|
||||||
|
## What serial doesn't give you
|
||||||
|
|
||||||
|
This is the kind of bug that doesn't exist in protocols with proper framing. HTTP has `Content-Length`. TCP has packet boundaries. Even SLIP has escape sequences. RS-232/422/485 serial gives you a stream of bytes with no metadata. The firmware doesn't tell you "this response is 347 bytes long" or "the prompt starts here." You have to infer structure from content.
|
||||||
|
|
||||||
|
The Winegard firmware's prompts are *intended* to serve as response terminators — that's why they're there. But the firmware authors never had to write a general-purpose parser for their own output. They were writing for human operators using a terminal emulator, where "I can see the prompt, so the response is done" is handled by the human's eyes.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
If you're writing serial communication code for embedded consoles, the lesson is: never terminate on a single character. Terminate on a *pattern* — ideally one that includes context about where in the line you are. Even a simple "prompt must be at the start of a line" check eliminates most false positives.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Residual fragility
|
||||||
|
|
||||||
|
The current implementation is much more robust than the original, but it's still heuristic-based. A pathological firmware response that happens to end a line with `SOMEWORD>` and no brackets would be misidentified as a prompt. We haven't hit this in practice on the Winegard firmware, but the possibility exists.
|
||||||
|
|
||||||
|
The right long-term fix would be a timeout-based fallback: if we haven't seen a known prompt within N seconds, assume the response is complete. The current code does this (the `deadline` in `send_cmd`), but it's the slow path — you wait for the full timeout instead of detecting the prompt early. For interactive probing, the prompt detection is fast enough that the timeout rarely triggers. For automated scanning of hundreds of commands, every unnecessary timeout adds up.
|
||||||
|
|
||||||
|
For now, the three-layer approach handles everything the G2 firmware throws at us. And we know exactly which layer is doing the work for each submenu, because the prompts are named — `TRK>`, `MOT>`, `DVB>`, `NVS>`, `EE>`, `A3981>`, `ADC>`, `DIPSWITCH>`, `GPIO>`, `LATLON>`, `OS>`, `PEAK>`, `STEP>`. Thirteen known prompts, all ending in `>`, all distinguishable from content. The naming convention that initially made parsing harder ended up being what made it solvable.
|
||||||
194
src/content/docs/journal/radio-telescope-mode.mdx
Normal file
194
src/content/docs/journal/radio-telescope-mode.mdx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
---
|
||||||
|
title: "Radio Telescope Mode"
|
||||||
|
description: Discovering azscanwxp and realizing this satellite TV dish has a built-in radio telescope capability
|
||||||
|
sidebar:
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
We were probing the MOT submenu when a command appeared in the help output that didn't seem to belong in a motor control menu: `azscanwxp`. The description said something about azimuth scanning with transponders. That turned out to be the most interesting command in the entire firmware.
|
||||||
|
|
||||||
|
## Finding azscanwxp
|
||||||
|
|
||||||
|
The MOT submenu help lists 25 commands. Most are what you'd expect — `a` for position, `g` for goto, `h` for home, `e` and `r` for engage/release, PID tuning, velocity control. Then there are two that stand out:
|
||||||
|
|
||||||
|
```
|
||||||
|
MOT> ?
|
||||||
|
...
|
||||||
|
azscan — AZ sweep with per-position RSSI readings
|
||||||
|
azscanwxp — AZ sweep with transponder cycling
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`azscan` sweeps the azimuth axis while recording signal strength at each position. `azscanwxp` does the same, but cycles through multiple DVB transponders at each position — so you get signal readings across multiple frequencies at each pointing angle.
|
||||||
|
|
||||||
|
This is a radio telescope scan pattern. Move the dish to a position, measure the signal at frequency 1, frequency 2, frequency 3, step to the next position, repeat. The output is a 2D grid of RF power measurements indexed by angle and frequency.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
The command takes four parameters:
|
||||||
|
|
||||||
|
```
|
||||||
|
azscanwxp [motor] [span] [resolution] [num_xponders]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Type | Units | Meaning |
|
||||||
|
|-----------|------|-------|---------|
|
||||||
|
| motor | int | -- | Motor axis (0=AZ, 1=EL) |
|
||||||
|
| span | float | degrees | Total sweep range |
|
||||||
|
| resolution | int | centidegrees | Step size (100 = 1.00 degree) |
|
||||||
|
| num_xponders | int | -- | Transponders to cycle per position |
|
||||||
|
|
||||||
|
So `azscanwxp 0 10 100 3` means: sweep 10 degrees on the azimuth axis, stepping 1.00 degree at a time, measuring 3 transponders at each stop. That's 10 positions with 3 readings each — 30 data points in one sweep.
|
||||||
|
|
||||||
|
The output format (from the ADC `scan` documentation, which uses the same reporting):
|
||||||
|
|
||||||
|
```
|
||||||
|
Motor:0 Angle:18000 RSSI:523 Lock:0 SNR:0 Scan Delta:100
|
||||||
|
Motor:0 Angle:18100 RSSI:519 Lock:0 SNR:0 Scan Delta:100
|
||||||
|
Motor:0 Angle:18200 RSSI:547 Lock:1 SNR:8 Scan Delta:100
|
||||||
|
```
|
||||||
|
|
||||||
|
Angle in centidegrees, RSSI as a raw ADC count, lock status (0 or 1), SNR in dB when locked, and the step size. Each line is one measurement at one position on one transponder.
|
||||||
|
|
||||||
|
<Aside type="danger" title="Safety: homed motors required">
|
||||||
|
`azscanwxp` targets motor positions by absolute angle. If the motor axis is uncalibrated (unhomed), the firmware may interpret the position register's INT_MAX sentinel value (2147483647) as a target angle and attempt to drive the motor there. This **deadlocks the firmware shell** — the motor task blocks forever, no serial input is processed, and only a hardware power cycle recovers.
|
||||||
|
|
||||||
|
Always home both axes before running scan commands: `h 0` then `h 1` in the MOT submenu.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## The connection to winegard-sky-scan
|
||||||
|
|
||||||
|
Chris Davidson's [winegard-sky-scan](https://github.com/cdavidson0522/winegard-sky-scan) project was already using this. His code automates the G2 over RS-422 to perform azimuth sweeps at multiple elevation angles, collecting RSSI data into grids that can be rendered as sky maps. He was using the Carryout G2 as a radio telescope before we knew the firmware had a dedicated command for it.
|
||||||
|
|
||||||
|
Davidson's approach drives the motors and reads RSSI separately — send a move command, wait for it to complete, query the signal. The `azscanwxp` command is the firmware doing both in a single coordinated operation: move, measure, advance, measure, advance. Less serial traffic, more precise timing, and the firmware handles the motor-to-measurement synchronization internally.
|
||||||
|
|
||||||
|
## The signal chain
|
||||||
|
|
||||||
|
To understand what RSSI means on the G2, you need to understand the signal path. The dish has a standard Ku-band LNB (Low Noise Block) at the focal point. The LNB does three things:
|
||||||
|
|
||||||
|
1. **Amplifies** the incoming RF signal (10.7-12.75 GHz range) with a low-noise amplifier
|
||||||
|
2. **Downconverts** it to an intermediate frequency (950-2150 MHz) by mixing with a local oscillator
|
||||||
|
3. **Sends** the IF signal down the coax cable to the BCM4515 tuner on the main board
|
||||||
|
|
||||||
|
The BCM4515 DVB-S2 demodulator then:
|
||||||
|
|
||||||
|
1. Selects a specific frequency (transponder) from the IF band
|
||||||
|
2. Demodulates the DVB-S2 signal (carrier tracking, symbol timing, FEC)
|
||||||
|
3. Reports signal quality metrics: RSSI, SNR, lock status, AGC levels
|
||||||
|
|
||||||
|
So when `azscanwxp` reports `RSSI:523`, that's the BCM4515's measurement of received power at the selected transponder frequency, after the LNB's amplification and downconversion. It's not a raw antenna voltage — it's a processed measurement from a purpose-built signal analysis chip.
|
||||||
|
|
||||||
|
## RSSI: two flavors
|
||||||
|
|
||||||
|
The firmware provides RSSI readings through two different paths, and they have different characteristics:
|
||||||
|
|
||||||
|
**DVB RSSI** (`dvb rssi <n>`) — reads the BCM4515's internal signal strength register, averaged over `n` samples. This is bounded (the command returns after `n` readings) and gives both average and current values:
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> rssi 10
|
||||||
|
Reads:10 RSSI[avg: 512 cur: 507]
|
||||||
|
```
|
||||||
|
|
||||||
|
The noise floor here is around 500. Anything above ~550 is signal, not noise.
|
||||||
|
|
||||||
|
**ADC RSSI** (`adc rssi`) — reads the raw ADC channel connected to the signal detector, bypassing the BCM4515's processing. The noise floor is lower (~233-238), and the readings are faster but noisier:
|
||||||
|
|
||||||
|
```
|
||||||
|
ADC> rssi
|
||||||
|
RSSI: 237
|
||||||
|
```
|
||||||
|
|
||||||
|
There's also `adc m` which streams continuous RSSI readings using carriage-return overwriting (the cursor stays on the same line, values update in place). Good for real-time monitoring, less useful for automated capture.
|
||||||
|
|
||||||
|
For sky mapping, the DVB RSSI path is better — it's already averaged, it's frequency-selective (tied to the selected transponder), and the `azscanwxp` command uses it internally.
|
||||||
|
|
||||||
|
## LNB polarization control
|
||||||
|
|
||||||
|
The LNB supports two polarizations — horizontal (H-pol, 18V bias) and vertical (V-pol, 13V bias). The polarization is selected by the DC voltage on the coax. The firmware provides control:
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> lnbdc odu
|
||||||
|
```
|
||||||
|
|
||||||
|
This sets the LNB to 13V (V-pol) in "ODU mode." The boot default is 18V (H-pol). The voltage determines which set of transponders you can receive — satellite TV uses both polarizations to double the available bandwidth.
|
||||||
|
|
||||||
|
For radio astronomy, polarization matters because celestial sources have polarization signatures. The PEAK submenu's `rssits` command exploits this:
|
||||||
|
|
||||||
|
```
|
||||||
|
PEAK> rssits
|
||||||
|
Even_sig = 489, Odd_sig = 235
|
||||||
|
Even_sig = 491, Odd_sig = 238
|
||||||
|
Even_sig = 487, Odd_sig = 233
|
||||||
|
```
|
||||||
|
|
||||||
|
It alternates between H-pol (18V, even transponders) and V-pol (13V, odd transponders), reporting signal strength for each. The asymmetry in the noise floor (489 vs 235) is the LNB's gain difference between polarizations — the amplifier chain isn't identical for both.
|
||||||
|
|
||||||
|
## DiSEqC: controlling the LNB from firmware
|
||||||
|
|
||||||
|
The BCM4515 includes a DiSEqC 2.x controller. DiSEqC (Digital Satellite Equipment Control) uses 22 kHz tone bursts superimposed on the coax DC bias to send commands to the LNB. In a typical satellite TV installation, this controls switches (selecting between multiple LNBs), band selection (low/high band on the same LNB), and polarity.
|
||||||
|
|
||||||
|
The DVB submenu exposes the full DiSEqC interface:
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> ovraddr
|
||||||
|
Override Address: 0x11
|
||||||
|
DVB> rrto
|
||||||
|
Rx Reply Timeout: 210 ms
|
||||||
|
DVB> pretx
|
||||||
|
Pre-Tx Delay: 15 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Address 0x11 is the standard first-LNB address. The timeout and delay values are within DiSEqC spec. The firmware can send raw DiSEqC packets:
|
||||||
|
|
||||||
|
```
|
||||||
|
DVB> send E0 10 38 F0
|
||||||
|
```
|
||||||
|
|
||||||
|
That's a standard DiSEqC 1.x command: framing byte E0, address 10 (any LNB), command 38 (Write N0), data F0 (port 1, option A, low band, V-pol). The combination of `lnbdc odu` (coax voltage control) and DiSEqC commands gives full control over the LNB's operating mode without rewiring anything.
|
||||||
|
|
||||||
|
For our purposes, the important part is frequency selection. The LNB's local oscillator frequency determines which RF band gets downconverted to the IF passband. By controlling the 22 kHz tone (band select) and coax voltage (polarity), we can shift the LNB's reception window across the Ku band. Combined with the BCM4515's internal frequency selection (transponder tuning), this gives us coverage across roughly 10.7-12.75 GHz.
|
||||||
|
|
||||||
|
## What this means for amateur radio
|
||||||
|
|
||||||
|
A Ku-band LNB on a motorized dish with RSSI readings is, functionally, a radio telescope. Not a particularly sensitive one — the LNB noise figure is around 0.5-0.7 dB, which is good for satellite TV but nowhere near what a proper radio astronomy receiver achieves. But it's enough to detect strong sources.
|
||||||
|
|
||||||
|
**Sun transit detection.** The sun is a powerful broadband RF source. At Ku band, solar flux is high enough that pointing the dish at the sun produces a dramatic RSSI increase — easily 10-20 dB above the noise floor. An azimuth scan around solar noon should show a clear peak at the sun's azimuth.
|
||||||
|
|
||||||
|
**Moon transit.** Weaker than the sun but detectable with averaging. The moon's thermal emission at 12 GHz produces a few dB of signal above noise. Longer integration times (more RSSI samples per position) or repeated scans averaged together would pull this out.
|
||||||
|
|
||||||
|
**Geostationary satellites.** These are the easy targets — they're literally what the dish was designed to see. A Ku-band azscan across the geostationary arc produces a forest of RSSI peaks, one per satellite. The Lock and SNR fields in the scan output distinguish between "there's RF power here" (RSSI) and "there's a decodable DVB signal here" (Lock=1, SNR > 0).
|
||||||
|
|
||||||
|
**Radio source imaging.** Run azscanwxp at multiple elevation angles and you get a 2D grid: azimuth on one axis, elevation on the other, RSSI as the value. Render it as a heatmap and you have a sky map at Ku band. Davidson's winegard-sky-scan project does exactly this.
|
||||||
|
|
||||||
|
## The scan workflow
|
||||||
|
|
||||||
|
Putting it together, a sky mapping session on the G2:
|
||||||
|
|
||||||
|
1. Home both motors: `mot` then `h 0`, `h 1`
|
||||||
|
2. Enable the LNA: `dvb` then `lnbdc odu`
|
||||||
|
3. Select a transponder frequency: `dvb` then `t <n>` to pick a transponder, or use `e` to set a specific frequency
|
||||||
|
4. Return to MOT: `q` then `mot`
|
||||||
|
5. Run the scan: `azscanwxp 0 <span> <resolution> <num_xponders>`
|
||||||
|
6. Capture output (serial terminal logging or script)
|
||||||
|
7. Adjust elevation, repeat
|
||||||
|
|
||||||
|
Each scan produces a one-dimensional strip. Stack strips at different elevations and you build a 2D image. The resolution trades off against scan time — 100 centidegrees (1.0 degree) steps are fast but coarse, 10 centidegrees (0.1 degree) steps give finer angular resolution but take 10x longer.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The dish's beam width at Ku band is roughly 2-3 degrees (determined by the reflector diameter and frequency). There's no point stepping finer than about 0.5 degrees — you'd be oversampling the beam pattern. For 1-degree resolution strips stacked at 1-degree elevation increments, a 90-degree azimuth sweep at 5 elevations takes about 15 minutes.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## What we haven't tried yet
|
||||||
|
|
||||||
|
The `azscanwxp` command is the firmware's built-in version of what Davidson's code does manually. We haven't run a full sky map with it yet — so far we've confirmed it executes, verified the output format, and tested short sweeps to check motor-RSSI synchronization.
|
||||||
|
|
||||||
|
The next steps are practical:
|
||||||
|
|
||||||
|
- **Calibration scan:** Point the dish at a known geostationary satellite and verify the RSSI peak aligns with the expected azimuth/elevation. This validates the motor position accuracy end-to-end.
|
||||||
|
- **Solar scan:** Sweep through the sun's position and measure the peak-to-noise ratio. This establishes the system's sensitivity floor.
|
||||||
|
- **Full sky map:** Raster scan a large area (maybe 90 degrees azimuth by 30 degrees elevation) and render it as a heatmap. This is the proof-of-concept for using the dish as an RF imager.
|
||||||
|
- **Multi-frequency mapping:** Use `num_xponders > 1` to get simultaneous data at multiple frequencies. Different celestial sources have different spectral profiles — the sun is broadband, satellites are narrowband, atmospheric attenuation varies with frequency.
|
||||||
|
|
||||||
|
The hardware is capable. The firmware commands exist. The mechanical platform (45 lbs, 33" x 23" reflector, 0-455 degree azimuth, 18-65 degree elevation) can point at most of the sky visible from the installation site. The gap is software to orchestrate the scans, capture the data, and render the results. That's what `birdcage` is building toward — not just satellite tracking, but RF imaging with surplus satellite TV hardware.
|
||||||
355
src/content/docs/reference/birdcage-api.mdx
Normal file
355
src/content/docs/reference/birdcage-api.mdx
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
---
|
||||||
|
title: "birdcage API"
|
||||||
|
description: Python API reference for the birdcage package — protocol abstraction, antenna control, rotctld server, leap-frog compensation, and CLI.
|
||||||
|
sidebar:
|
||||||
|
order: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The `birdcage` package provides a layered Python API for controlling Winegard satellite dishes. The architecture separates firmware communication (protocol), high-level control (antenna), network integration (rotctld), and predictive compensation (leapfrog).
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
cli.py Click CLI with init/serve/pos/move subcommands
|
||||||
|
|
|
||||||
|
v
|
||||||
|
antenna.py BirdcageAntenna: high-level control (consumers call this)
|
||||||
|
|
|
||||||
|
+---> leapfrog.py Pure function: apply_leapfrog(target, current)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
protocol.py FirmwareProtocol ABC + HAL205Protocol / HAL000Protocol / CarryoutG2Protocol
|
||||||
|
Serial I/O owned here. Each firmware version is a subclass.
|
||||||
|
|
|
||||||
|
v
|
||||||
|
rotctld.py RotctldServer: Hamlib rotctld TCP protocol (p/P/S/_/q/R/L/D)
|
||||||
|
Bridges Gpredict to the antenna.
|
||||||
|
```
|
||||||
|
|
||||||
|
## protocol.py -- Firmware Protocol Abstraction
|
||||||
|
|
||||||
|
### Data Classes
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class Position:
|
||||||
|
"""Current dish orientation."""
|
||||||
|
azimuth: float
|
||||||
|
elevation: float
|
||||||
|
skew: float | None = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RssiReading:
|
||||||
|
"""Signal strength reading from the DVB subsystem."""
|
||||||
|
reads: int
|
||||||
|
average: int
|
||||||
|
current: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
```python
|
||||||
|
MOTOR_AZIMUTH = 0
|
||||||
|
MOTOR_ELEVATION = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### FirmwareProtocol (ABC)
|
||||||
|
|
||||||
|
The abstract base class that all firmware implementations inherit from. Owns the serial connection and defines the contract for firmware interaction.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class FirmwareProtocol(ABC):
|
||||||
|
def connect(self, port: str, baudrate: int = 57600) -> None: ...
|
||||||
|
def disconnect(self) -> None: ...
|
||||||
|
def reset_to_root(self) -> None: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def initialize(self, callback: Callable[[str], None] | None = None) -> None: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def enter_motor_menu(self) -> None: ...
|
||||||
|
|
||||||
|
def get_position(self) -> Position: ...
|
||||||
|
def move_motor(self, motor_id: int, degrees: float) -> None: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def kill_search(self) -> None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key methods:**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `connect(port, baudrate)` | Open the serial connection (8N1, 1s timeout) |
|
||||||
|
| `disconnect()` | Reset to root menu and close serial |
|
||||||
|
| `initialize(callback)` | Wait for boot, kill satellite search. Callback receives status lines. |
|
||||||
|
| `enter_motor_menu()` | Navigate into the motor control submenu |
|
||||||
|
| `get_position()` | Query current AZ/EL/SK position (returns `Position`) |
|
||||||
|
| `move_motor(motor_id, degrees)` | Command a single motor to an absolute position |
|
||||||
|
| `kill_search()` | Cancel the firmware's automatic TV satellite search |
|
||||||
|
| `reset_to_root()` | Send `q` to return to root menu |
|
||||||
|
|
||||||
|
### HAL205Protocol
|
||||||
|
|
||||||
|
HAL 2.05.003 firmware implementation.
|
||||||
|
|
||||||
|
- **Boot signals:** `"NoGPS"` or `"No LNB Voltage"`
|
||||||
|
- **Motor submenu command:** `"motor"`
|
||||||
|
- **Search kill sequence:** `ngsearch` -> `s` -> `q`
|
||||||
|
- **Default baud:** 57600
|
||||||
|
|
||||||
|
### HAL000Protocol
|
||||||
|
|
||||||
|
HAL 0.0.00 firmware implementation (older Trav'ler units).
|
||||||
|
|
||||||
|
- **Boot signal:** `"NoGPS"`
|
||||||
|
- **Motor submenu command:** `"mot"`
|
||||||
|
- **Search kill sequence:** `os` -> `kill Search` -> `q` (OS task manager approach)
|
||||||
|
- **Default baud:** 57600
|
||||||
|
|
||||||
|
### CarryoutG2Protocol
|
||||||
|
|
||||||
|
Winegard Carryout G2 firmware implementation. Extends the base protocol with prompt-terminated reads, DVB signal measurement, and motor homing.
|
||||||
|
|
||||||
|
- **Default baud:** 115200
|
||||||
|
- **Motor submenu command:** `"mot"`
|
||||||
|
- **Search disable:** NVS index 20 (permanent, no runtime kill needed)
|
||||||
|
- **Read strategy:** Reads byte-by-byte until `>` (ASCII 62) prompt character
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CarryoutG2Protocol(FirmwareProtocol):
|
||||||
|
def connect(self, port: str, baudrate: int = 115200) -> None: ...
|
||||||
|
def get_position(self) -> Position: ... # Parses Angle[0]/Angle[1] format
|
||||||
|
def move_motor(self, motor_id: int, degrees: float) -> None: ...
|
||||||
|
def home_motor(self, motor_id: int) -> None: ...
|
||||||
|
|
||||||
|
# DVB / RSSI methods
|
||||||
|
def enter_dvb_menu(self) -> None: ...
|
||||||
|
def enable_lna(self) -> None: ...
|
||||||
|
def get_rssi(self, iterations: int = 10) -> RssiReading: ...
|
||||||
|
def quit_submenu(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**G2-specific methods:**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `home_motor(motor_id)` | Home a motor to its reference position via stall detection |
|
||||||
|
| `enter_dvb_menu()` | Enter the DVB signal analysis submenu |
|
||||||
|
| `enable_lna()` | Enable LNA in ODU mode (13V) for signal reception |
|
||||||
|
| `get_rssi(iterations)` | Read averaged RSSI signal strength (returns `RssiReading`) |
|
||||||
|
| `quit_submenu()` | Exit current submenu |
|
||||||
|
|
||||||
|
### Firmware Registry
|
||||||
|
|
||||||
|
```python
|
||||||
|
FIRMWARE_REGISTRY: dict[str, type[FirmwareProtocol]] = {
|
||||||
|
"hal205": HAL205Protocol,
|
||||||
|
"hal000": HAL000Protocol,
|
||||||
|
"g2": CarryoutG2Protocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_protocol(name: str) -> FirmwareProtocol:
|
||||||
|
"""Instantiate a firmware protocol by short name.
|
||||||
|
Raises KeyError if name is not recognized.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## antenna.py -- High-Level Antenna Control
|
||||||
|
|
||||||
|
### AntennaConfig
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class AntennaConfig:
|
||||||
|
port: str = "/dev/ttyUSB0"
|
||||||
|
baudrate: int = 57600
|
||||||
|
min_elevation: float = 15.0
|
||||||
|
leapfrog_enabled: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### BirdcageAntenna
|
||||||
|
|
||||||
|
The main interface that consumers (CLI, rotctld server, future MCP server) should use. Wraps the firmware protocol with position tracking, leap-frog compensation, and motor command alternation.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BirdcageAntenna:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
protocol: FirmwareProtocol,
|
||||||
|
config: AntennaConfig | None = None,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> AntennaConfig: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protocol(self) -> FirmwareProtocol: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool: ...
|
||||||
|
|
||||||
|
def connect(self) -> None: ...
|
||||||
|
def disconnect(self) -> None: ...
|
||||||
|
def initialize(self) -> None: ...
|
||||||
|
def get_position(self) -> Position: ...
|
||||||
|
def move_to(self, azimuth: float, elevation: float) -> None: ...
|
||||||
|
def stop(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key behaviors:**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `initialize()` | Connect (if needed), run boot sequence, enter motor menu |
|
||||||
|
| `get_position()` | Query position and cache as `_last_position` for leapfrog |
|
||||||
|
| `move_to(az, el)` | Move with leapfrog compensation + elevation floor enforcement. Motor commands alternate order (AZ-first on even moves, EL-first on odd) to prevent one axis from starving. |
|
||||||
|
| `stop()` | Reset move counter |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Motor commands are alternated between AZ-first and EL-first on successive moves. This prevents one axis from consistently being delayed by the other on the shared serial bus. The `move_to` method tracks this via an internal `_move_count`.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## leapfrog.py -- Predictive Overshoot
|
||||||
|
|
||||||
|
A pure function that compensates for mechanical motor lag during satellite tracking. When the delta between target and current position exceeds a threshold, the target is nudged further in the direction of travel.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def apply_leapfrog(
|
||||||
|
target_az: float,
|
||||||
|
target_el: float,
|
||||||
|
current_az: float,
|
||||||
|
current_el: float,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""Apply predictive overshoot to compensate for mechanical lag.
|
||||||
|
|
||||||
|
Returns adjusted (azimuth, elevation) with overshoot applied.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Compensation table:**
|
||||||
|
|
||||||
|
| Delta | Overshoot applied |
|
||||||
|
|-------|------------------|
|
||||||
|
| More than 2 degrees | +/- 1.0 degree in direction of travel |
|
||||||
|
| More than 1 degree | +/- 0.5 degree in direction of travel |
|
||||||
|
| 1 degree or less | No overshoot |
|
||||||
|
|
||||||
|
<Aside type="note" title="Bug fix from upstream">
|
||||||
|
The original upstream code had a copy-paste bug where the elevation delta adjustments modified `target_az` instead of `target_el` (lines 98-105 of `travler_rotor.py`). This bug is present in both the Trav'ler and Trav'ler Pro repositories. Fixed in this implementation.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## rotctld.py -- Hamlib rotctld Server
|
||||||
|
|
||||||
|
Implements the subset of the Hamlib rotctld TCP protocol that Gpredict and other Hamlib clients use for AZ/EL rotor control. Extended with custom commands for sky-scan integration on the Carryout G2.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RotctldServer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
antenna: BirdcageAntenna,
|
||||||
|
host: str = "127.0.0.1",
|
||||||
|
port: int = 4533,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
def serve_forever(self) -> None: ...
|
||||||
|
def shutdown(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard rotctld Commands
|
||||||
|
|
||||||
|
| Command | Description | Response |
|
||||||
|
|---------|-------------|----------|
|
||||||
|
| `p` | Get position | `<azimuth>\n<elevation>\n` |
|
||||||
|
| `P <az> <el>` | Set position (move dish) | `RPRT 0\n` on success |
|
||||||
|
| `S` | Stop tracking | Calls `antenna.stop()` |
|
||||||
|
| `_` | Get model name | `Winegard Trav'ler RS-485 Rotor\n` |
|
||||||
|
| `q` | Quit connection | Closes the TCP connection |
|
||||||
|
|
||||||
|
### Extended Commands (Carryout G2 only)
|
||||||
|
|
||||||
|
These custom extensions are available when the underlying protocol is `CarryoutG2Protocol`. Non-G2 rotors return `RPRT -6` (not available).
|
||||||
|
|
||||||
|
| Command | Description | Response |
|
||||||
|
|---------|-------------|----------|
|
||||||
|
| `R [n]` | Read RSSI signal strength (n = iterations, default 10) | `<reads>\n<average>\n<current>\n` |
|
||||||
|
| `L` | Enable LNA for signal reception (one-time setup) | `RPRT 0\n` on success |
|
||||||
|
| `D` | Discover supported capabilities | `CAPS:rssi,lna\n` (G2) or `CAPS:\n` |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The `R` (RSSI) command handles DVB menu switching internally: it exits the motor menu, enters the DVB menu, reads RSSI, exits DVB, and re-enters the motor menu. This means an RSSI read temporarily interrupts motor command availability.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## cli.py -- Command-Line Interface
|
||||||
|
|
||||||
|
Built with [Click](https://click.palletsprojects.com/). All commands accept `--port` and `--firmware` options (also configurable via `BIRDCAGE_PORT` and `BIRDCAGE_FIRMWARE` environment variables).
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
#### `birdcage init`
|
||||||
|
|
||||||
|
Initialize the antenna: wait for boot, kill satellite search.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
birdcage init --port /dev/ttyUSB0 --firmware hal205
|
||||||
|
birdcage init --port /dev/ttyUSB2 --firmware g2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `birdcage serve`
|
||||||
|
|
||||||
|
Run a rotctld-compatible TCP server for Gpredict.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
birdcage serve --port /dev/ttyUSB0 --firmware hal205
|
||||||
|
birdcage serve --port /dev/ttyUSB2 --firmware g2 --host 0.0.0.0 --listen-port 4533
|
||||||
|
birdcage serve --port /dev/ttyUSB2 --firmware g2 --skip-init
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--host` | 127.0.0.1 | Address to listen on |
|
||||||
|
| `--listen-port` | 4533 | TCP port for rotctld protocol |
|
||||||
|
| `--skip-init` | false | Skip boot wait and search kill (dish already initialized) |
|
||||||
|
|
||||||
|
#### `birdcage pos`
|
||||||
|
|
||||||
|
Query and print the current dish position.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
birdcage pos --port /dev/ttyUSB2 --firmware g2
|
||||||
|
# Output:
|
||||||
|
# AZ: 180.0
|
||||||
|
# EL: 45.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `birdcage move`
|
||||||
|
|
||||||
|
Move the dish to a specific AZ/EL position.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
birdcage move --port /dev/ttyUSB2 --firmware g2 --az 180.0 --el 45.0
|
||||||
|
birdcage move --port /dev/ttyUSB0 --firmware hal205 --az 90.0 --el 30.0 --no-leapfrog
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--az` | Target azimuth (degrees, required) |
|
||||||
|
| `--el` | Target elevation (degrees, required) |
|
||||||
|
| `--no-leapfrog` | Disable leap-frog compensation for this move |
|
||||||
|
|
||||||
|
### Global Options
|
||||||
|
|
||||||
|
| Option | Env Var | Default | Description |
|
||||||
|
|--------|---------|---------|-------------|
|
||||||
|
| `-v` / `--verbose` | -- | false | Enable debug logging |
|
||||||
|
| `--port` | `BIRDCAGE_PORT` | /dev/ttyUSB0 | Serial port |
|
||||||
|
| `--firmware` | `BIRDCAGE_FIRMWARE` | hal205 | Firmware version (hal205, hal000, g2) |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
When using the `g2` firmware, the CLI automatically defaults to 115200 baud and 18 degree minimum elevation. For HAL variants, the defaults are 57600 baud and 15 degree minimum.
|
||||||
|
</Aside>
|
||||||
442
src/content/docs/reference/console-commands.mdx
Normal file
442
src/content/docs/reference/console-commands.mdx
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
---
|
||||||
|
title: "Console Commands"
|
||||||
|
description: Complete firmware console command inventory for the Winegard Carryout G2 — all 12 submenus, 100+ commands documented from firmware 02.02.48.
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Full command inventory from automated deep probe + interactive `?` exploration (firmware 02.02.48, captured 2026-02-12). Automated probing finds commands that respond without arguments; interactive `?` in each submenu reveals the full set including parameter-requiring commands the probe misses.
|
||||||
|
|
||||||
|
<Aside type="danger" title="Known Console Hazards">
|
||||||
|
**ADC `scan` without arguments on uncalibrated AZ:** Targets position 2147483647 (INT_MAX), motor task blocks forever, shell deadlocks. No serial input (CR, Ctrl+C, ESC, `q`, `reboot`) can recover -- requires hardware power cycle. The firmware shell is single-threaded: UART input is only parsed between command completions, so a blocking motor move prevents all input.
|
||||||
|
|
||||||
|
**Root `q` command:** Terminates the shell task entirely. Console becomes unresponsive until power cycle.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Root Menu (TRK>)
|
||||||
|
|
||||||
|
The root prompt provides access to 12 submenus plus system commands.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `?` | List available commands (alias: `help`) |
|
||||||
|
| `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 |
|
||||||
|
| `os` | Enter OS submenu |
|
||||||
|
| `peak` | Enter peak/DiSEqC switch submenu |
|
||||||
|
| `step` | Enter stepper motor submenu |
|
||||||
|
| `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) |
|
||||||
|
|
||||||
|
<Aside type="danger">
|
||||||
|
The `q` command at the root menu terminates the shell task entirely. The console becomes unresponsive and requires a hardware power cycle to recover. Only use `q` inside submenus to return to the parent menu.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
`command` appeared in automated probe results -- this is a false positive. The help parser extracted it from the `help [<command>]` usage text, where `<command>` is a parameter placeholder, not an actual command.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A3981 Submenu (A3981>) -- Allegro Stepper Driver
|
||||||
|
|
||||||
|
6 commands. Controls the two A3981 stepper motor driver ICs via SPI.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `cm` | Current control mode: AZ/EL both report "AUTO" or "HiZ"/"LoZ" |
|
||||||
|
| `diag` | Fault pin status: "AZ DIAG: OK EL DIAG: OK" (or FAULT) |
|
||||||
|
| `reset` | Reset AZ/EL A3981 fault flags |
|
||||||
|
| `sm` | Step size mode: AZ/EL both report "AUTO" or fixed mode |
|
||||||
|
| `ss` | Step size: returns integer (FULL=16, HALF=8, QTR=4, EIGHTH=2, SIXTEENTH=1) |
|
||||||
|
| `st` | Torque level: AZ/EL report "HIGH" (moving) or "LOW" (idle/holding) |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
The step size value is an inverted divisor -- `1` means 1/16 microstepping (finest), `16` means full stepping (coarsest). In AUTO mode, the A3981 dynamically adjusts microstepping based on motor speed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADC Submenu (ADC>) -- Analog-to-Digital Converter
|
||||||
|
|
||||||
|
5 commands. Hardware-level ADC readings from the LNB signal chain and board ID.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `bdid` | Board identity: returns "STATIONARY" (Carryout G2 variant) |
|
||||||
|
| `bdrevid` | Board revision: returns "A" |
|
||||||
|
| `m` | Monitor RSSI (streaming, CR-overwrite line, interrupt with q) |
|
||||||
|
| `rssi` | Single-shot RSSI (raw ADC count, ~233-238 noise floor) |
|
||||||
|
| `scan` | AZ sweep with per-position RSSI/Lock/SNR readings |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
The `scan` command outputs: `Motor:<id> Angle:<cdeg> RSSI:<adc> Lock:<0/1> SNR:<dB> Scan Delta:<step>`
|
||||||
|
|
||||||
|
<Aside type="danger" title="scan deadlock">
|
||||||
|
Running `scan` without arguments on an uncalibrated AZ axis targets INT_MAX (2147483647), which deadlocks the shell permanently. The only recovery is a hardware power cycle. Always home the motors before using `scan`.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DIPSWITCH Submenu (DIPSWITCH>)
|
||||||
|
|
||||||
|
1 command. Reads physical DIP switch GPIOs and interprets satellite config code.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `dipswitch` | Read dipswitch: `val:<hex32>` (raw GPIO) + `app_dipswitch:<decimal>` (interpreted) |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
Example: `val:ffffff01` = all switches OFF/up. `app_dipswitch:101` = DISH 110+119+129 degrees W.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DVB Submenu (DVB>) -- BCM4515 Tuner
|
||||||
|
|
||||||
|
38 commands. Controls the Broadcom BCM4515 DVB-S2 tuner and DiSEqC 2.x LNB interface. Help is paginated: `?` shows the first page, `man` shows extended commands.
|
||||||
|
|
||||||
|
#### Signal Measurement
|
||||||
|
|
||||||
|
| Command | Type | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `rssi <n>` | One-shot | RSSI averaged over n samples. Returns `Reads:<n> RSSI[avg: <v> cur: <v>]` |
|
||||||
|
| `snr` | Streaming | SNR level (avg + current). Interrupt with q |
|
||||||
|
| `agc` | Streaming | RF/IF AGC + SNR + NID. Interrupt with q |
|
||||||
|
| `qls` | Streaming | Quick lock status at ~100ms intervals: `Lock:0 rssi:500 cnt:0` |
|
||||||
|
| `nid` | Streaming | Network ID (CR-overwrite display, FFFF = no signal) |
|
||||||
|
| `stats` | Streaming | Accumulated signal statistics (silent when no lock) |
|
||||||
|
|
||||||
|
#### Channel Configuration
|
||||||
|
|
||||||
|
| Command | Type | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `dis` | One-shot | Display channel parameters (frequency, symbol rate, LNB polarity) |
|
||||||
|
| `e <n> <v>` | Write | Edit channel parameter |
|
||||||
|
| `freqs` | One-shot | Tuner frequency list name |
|
||||||
|
| `range` | One-shot | Transponder scan range |
|
||||||
|
| `t <n>` | Write | Select transponder |
|
||||||
|
|
||||||
|
#### LNB / Power
|
||||||
|
|
||||||
|
| Command | Type | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `lnbdc odu` | One-shot | Enable LNA in ODU mode (13V = V-pol; boot default 18V = H-pol) |
|
||||||
|
| `lnbv` | Streaming | Stream LNB voltage readings. Interrupt with q |
|
||||||
|
| `pwr` | One-shot | SDS and DiSEqC core power state |
|
||||||
|
|
||||||
|
#### Scanning / Lock
|
||||||
|
|
||||||
|
| Command | Type | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `ls` | Streaming | Full transponder lock scan. **Exits DVB submenu on interrupt** |
|
||||||
|
| `config` | One-shot | BCM hardware/firmware version (ID 0x4515, Rev B0, FW v113.37) |
|
||||||
|
| `diag` | One-shot | Multi-block per-transponder diagnostics |
|
||||||
|
| `table` | One-shot | Full 32-transponder scan (~136 seconds) |
|
||||||
|
| `tablex` | Toggle | Extended table mode (on/off) |
|
||||||
|
| `srch` | Toggle | Search transponders mode (on/off) |
|
||||||
|
| `srch_mode` | One-shot | Auto search mode value |
|
||||||
|
| `shuf` | Toggle | Transponder shuffle order (on/off) |
|
||||||
|
| `def` | Write | Reset all params to defaults (silent, no confirmation, no undo) |
|
||||||
|
| `msw` | Write | Modulation switch (format unknown) |
|
||||||
|
|
||||||
|
#### Timeout Settings
|
||||||
|
|
||||||
|
| Command | Type | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `tabto` | Read/Write | Table scan timeouts (acq/nid/sd) |
|
||||||
|
| `to` | Read/Write | Single tune timeouts (acq/nid) |
|
||||||
|
|
||||||
|
#### DiSEqC 2.x Commands
|
||||||
|
|
||||||
|
| Command | Type | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `di2conf` | Read | DiSEqC LNB config register |
|
||||||
|
| `di2cs` | Write | DiSEqC committed switch command |
|
||||||
|
| `di2id` | Read | DiSEqC read LNB hardware ID |
|
||||||
|
| `di2rcs` | Read | DiSEqC read committed switch status |
|
||||||
|
| `di2sc` | Read | DiSEqC switch control |
|
||||||
|
| `di2stat` | Read | DiSEqC read LNB status flags |
|
||||||
|
| `ovraddr` | Read/Write | DiSEqC override address (default 0x11) |
|
||||||
|
| `pretx` | Read/Write | DiSEqC pre-transmit delay (default 15 ms) |
|
||||||
|
| `rrto` | Read/Write | DiSEqC receive reply timeout (default 210 ms) |
|
||||||
|
| `send <hex>` | Write | Raw DiSEqC packet (max 6 bytes, space-delimited hex) |
|
||||||
|
| `tdthresh` | Read/Write | DiSEqC tone detect threshold (default 110) |
|
||||||
|
|
||||||
|
#### Other
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `man` | Extended help page |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Streaming commands (`snr`, `agc`, `lnbv`, `qls`, `nid`, `stats`) run until CR interrupts them. All stay in the DVB submenu when interrupted **except** `ls`, which drops to `TRK>` (exits the DVB submenu entirely).
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The `def` command silently resets all channel parameters to defaults with no output, no confirmation, and no undo.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### EEPROM Submenu (EE>) -- K60 FlexNVM/EEPROM
|
||||||
|
|
||||||
|
3 commands. Low-level EEPROM access (separate from NVS). The prompt is `EE>`, not `EEPROM>`. Most indices read as 0 (unwritten) or fail with val:65793 (0x10101 marker = uninitialized). The firmware primarily uses NVS, not EEPROM, for persistent settings.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `ee <idx> [<v>]` | Read/write EEPROM value at index. Read: `Index:<n> Read value = <v>` or `Failed to read eeprom index:<n> val:65793` |
|
||||||
|
| `inv <idx>` | **INVALIDATE** EEPROM index (destructive -- marks entry invalid, not "inventory") |
|
||||||
|
| `def` | Restore EEPROM defaults |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
<Aside type="danger">
|
||||||
|
`inv` means **invalidate**, not "inventory". It destructively marks an EEPROM index as invalid by writing the 0x00010101 sentinel. There is no confirmation prompt. Use `def` to restore factory defaults if needed.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GPIO Submenu (GPIO>)
|
||||||
|
|
||||||
|
4 commands. Direct access to K60 GPIO ports A-E. Pin naming: `<port><pin>` (e.g., `B0`, `E12`).
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `dir <pin>` | Query pin direction: returns "INPUT" or "OUTPUT" |
|
||||||
|
| `r <pin>` | Read single GPIO pin value (0 or 1) |
|
||||||
|
| `regs` | Dump ALL GPIO pin states across ports A-E (26+16+20+16+14 = 92 pins) |
|
||||||
|
| `w <pin> <val>` | Write GPIO pin (requires pin name and value) |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Pins A20-A23 and B12-B15 are not enumerated (not bonded on the 144-LQFP package). E29 shows "Unknown bit E29" in the firmware output.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LATLON Submenu (LATLON>)
|
||||||
|
|
||||||
|
1 command. Satellite triangulation calculator -- computes ground station lat/lon from look angles to two known geostationary satellites. Used for auto-location when GPS is unavailable. Values stored internally as centidegrees.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `l <p1> <p2> <p3> <p4>` | Calculate lat/lon from 4 parameters (likely AZ/EL pairs for 2 satellites) |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
Output format:
|
||||||
|
```
|
||||||
|
anglesentered = <cdeg1> <cdeg2> <cdeg3> <cdeg4>
|
||||||
|
Lat = <cdeg> Lon = <cdeg>
|
||||||
|
```
|
||||||
|
|
||||||
|
Values are in centidegrees: `Lat = 4295` means 42.95 degrees N, `Lon = 25655` means 256.55 degrees E (= 103.45 degrees W).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MOT Submenu (MOT>) -- Motor Control
|
||||||
|
|
||||||
|
25 commands. High-level motor control with angle-based positioning.
|
||||||
|
|
||||||
|
#### Position and Movement
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `a` | Show position: `Angle[0]` (AZ), `Angle[1]` (EL) |
|
||||||
|
| `a <id> <deg>` | Move motor to absolute angle (0=AZ, 1=EL) |
|
||||||
|
| `a <id> +/-deg` | Relative move (G2 only, undocumented in upstream) |
|
||||||
|
| `g <az> <el>` | Go to AZ/EL (aborts on new input) |
|
||||||
|
| `h <id>` | Home motor to reference position (stall-detect based) |
|
||||||
|
| `e` | Engage motors (energize steppers) |
|
||||||
|
| `r` | Release motors (de-energize steppers) |
|
||||||
|
|
||||||
|
#### Information Queries
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `l` | List motors and state (0=AZIMUTH, 1=ELEVATION) |
|
||||||
|
| `p` | Read raw step positions |
|
||||||
|
| `v` | Read motor velocities |
|
||||||
|
| `ma` | Read max acceleration per motor |
|
||||||
|
| `mv` | Max velocity per motor |
|
||||||
|
| `elminmaxhome` | Show EL limits: `Min: <v> Max: <v> Home: <v>` (NVS values) |
|
||||||
|
| `ela2s <deg>` | Elevation angle to steps converter |
|
||||||
|
| `els2a <steps>` | Elevation steps to angle converter |
|
||||||
|
|
||||||
|
#### Scanning
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `azscan [az_rel] [el_rel] [delay]` | AZ sweep: scan relative AZ range at EL steps with delay |
|
||||||
|
| `azscanwxp [motor] [span_deg] [res_cdeg] [num_xponders]` | AZ sweep + transponder cycling (radio telescope mode) |
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `pid [motor] [Kp] [Kv] [Ki]` | Read or set PID gains for motor control loop |
|
||||||
|
| `sp [motor] [pos]` | Set position (override current position register) |
|
||||||
|
| `sw [motor] [pos]` | Set wrap position (cable wrap reference point) |
|
||||||
|
| `w [motor] [ON/OFF]` | Wrap manager: enable/disable cable wrap protection per motor |
|
||||||
|
| `vms [motor] [deg_per_rev] [ms]` | Velocity move for duration |
|
||||||
|
|
||||||
|
#### Testing
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `life` | Motor lifetime/usage stats |
|
||||||
|
| `motorboth` | Simultaneous dual-motor move test |
|
||||||
|
| `motorlife` | Detailed motor life statistics |
|
||||||
|
| `sd` | Stall detection test (motor, direction, timeout) |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
#### azscanwxp -- Radio Telescope Mode
|
||||||
|
|
||||||
|
The `azscanwxp` command performs an azimuth sweep while cycling through DVB transponders at each position. This is the core of Davidson's winegard-sky-scan project for RF imaging.
|
||||||
|
|
||||||
|
| Parameter | Type | Units | Description |
|
||||||
|
|-----------|------|-------|-------------|
|
||||||
|
| motor | int | -- | Motor ID (0=AZ, 1=EL) |
|
||||||
|
| span | float | degrees | Total azimuth sweep range |
|
||||||
|
| resolution | int | centidegrees | Step size per position |
|
||||||
|
| num_xponders | int | -- | Number of transponders to cycle at each position |
|
||||||
|
|
||||||
|
Example: `azscanwxp 0 10 100 3` -- sweep 10 degrees on AZ at 1.00 degree steps, checking 3 transponders per position.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Requires homed motors. Do NOT run on uncalibrated axes -- the firmware may target INT_MAX (2147483647 steps) and deadlock the shell.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NVS Submenu (NVS>) -- Non-Volatile Storage
|
||||||
|
|
||||||
|
5 commands. See the [NVS Settings](/reference/nvs-settings/) page for a full index dump.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `d` | Dump all NVS values (name/current/saved/default) |
|
||||||
|
| `d <idx>` | Dump single value with details |
|
||||||
|
| `e <idx>` | Read NVS value at index |
|
||||||
|
| `e <idx> <v>` | Write NVS value at index (NOT saved until `s`) |
|
||||||
|
| `s` | Save pending changes to flash |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
NVS `e <idx> <value>` writes values immediately to the in-memory copy. Changes are NOT persisted to flash until you send `s`. Any unrecognized input is treated as a sequential index read (no error string), which generates false positives during probing but is harmless.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OS Submenu (OS>)
|
||||||
|
|
||||||
|
2 commands on the Carryout G2 (4 on HAL 0.0.00).
|
||||||
|
|
||||||
|
| Command | Description | Availability |
|
||||||
|
|---------|-------------|-------------|
|
||||||
|
| `id` | Full MCU/firmware identification (NVS version, System ID, chip) | All variants |
|
||||||
|
| `reboot` | Reboot microcontroller | All variants |
|
||||||
|
| `tasks` | List running tasks | HAL 0.0.00 only |
|
||||||
|
| `kill <name>` | Kill a named task | HAL 0.0.00 only |
|
||||||
|
| `?` / `q` | Help / return to TRK> | All variants |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
`tasks` and `kill` are not available on the Carryout G2. On the G2, the satellite search is disabled permanently via NVS index 20 instead of killing a running task.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PEAK Submenu (PEAK>) -- Signal Peak / DiSEqC Switch
|
||||||
|
|
||||||
|
6 commands. EchoStar/DiSEqC switch control and LNB polarity-switched RSSI.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `pw` | Peak signal search (likely requires satellite lock) |
|
||||||
|
| `psnr` | Peak SNR measurement |
|
||||||
|
| `pxy1` | Peak XY single-axis (2D cross-pattern peak search) |
|
||||||
|
| `rssits` | RSSI with LNB toggle switch: alternates H-pol (18V, even transponders) and V-pol (13V, odd transponders). Reports `Even_sig = <v>, Odd_sig = <v>`. Noise floor: even ~489, odd ~235 (V-pol quieter). **Exits submenu on interrupt.** |
|
||||||
|
| `stb` | STB (set-top box) control / DiSEqC switch test |
|
||||||
|
| `ts` | EchoStar switch toggle status: `SW Status: 0b<binary> <decimal>` (all zeros = no switch connected) |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The `ts` command runs indefinitely, probing for a DiSEqC/EchoStar switch by toggling LNB voltage. It cannot be stopped by sending `q` -- the running command consumes all input. Close and reopen the serial port to escape.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP Submenu (STEP>) -- Low-Level Stepper Control
|
||||||
|
|
||||||
|
7 commands. Raw stepper API in microstep units (ustep/sec, ustep/sec/msec). MOT wraps STEP with angle-to-step conversion.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `e` | Engage motor (same as MOT `e`) |
|
||||||
|
| `ma` | Max acceleration: `Accel[0] = 44 / Accel[1] = 28` (ustep/sec/ms). Set: `ma [motor] [ustep/sec/ms]` |
|
||||||
|
| `mv` | Max velocity: `Max Vel [0] = 7222 / Max Vel [1] = 3120` (ustep/sec). Set: `mv [motor] [ustep/sec]` |
|
||||||
|
| `p` | Goto position in raw step counts: `p [motor] [steps]` |
|
||||||
|
| `pid` | PID values: `Kp=250 Kv=50` (no Ki at STEP level). Set: `pid [motor] [Kp] [Kv]` |
|
||||||
|
| `r` | Release motors (same as MOT `r`) |
|
||||||
|
| `v` | Go to velocity (continuous spin): `v [motor] [ustep/sec]` |
|
||||||
|
| `?` / `q` | Help / return to TRK> |
|
||||||
|
|
||||||
|
#### STEP to MOT Conversion
|
||||||
|
|
||||||
|
The relationship between STEP microstep units and MOT degree units:
|
||||||
|
|
||||||
|
| Parameter | STEP value | MOT value | Conversion factor |
|
||||||
|
|-----------|-----------|----------|------------------|
|
||||||
|
| AZ velocity | 7222 ustep/s | 65.0 deg/s | 40000 steps/rev / 360 = 111.11 steps/deg |
|
||||||
|
| EL velocity | 3120 ustep/s | 45.0 deg/s | 24960 steps/rev / 360 = 69.33 steps/deg |
|
||||||
|
| AZ acceleration | 44 ustep/s/ms | ~396 deg/s^2 | x1000 / 111.11 |
|
||||||
|
| EL acceleration | 28 ustep/s/ms | ~404 deg/s^2 | x1000 / 69.33 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DiSEqC 2.x Interface
|
||||||
|
|
||||||
|
The BCM4515 provides a DiSEqC 2.x controller accessible from the DVB> submenu. DiSEqC (Digital Satellite Equipment Control) uses 22 kHz tone bursts on the coax LNB bias line to control switches, LNB polarity, and band selection.
|
||||||
|
|
||||||
|
### Timing Parameters (confirmed live)
|
||||||
|
|
||||||
|
| Command | Value | Description |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| `ovraddr` | 0x11 | Target LNB address (standard first LNB) |
|
||||||
|
| `rrto` | 210 ms | Receive reply timeout |
|
||||||
|
| `pretx` | 15 ms | Pre-command TX delay |
|
||||||
|
| `tdthresh` | 110 | Tone detect threshold (0.16 counts/mV) |
|
||||||
|
|
||||||
|
### DiSEqC Status
|
||||||
|
|
||||||
|
| Command | Function | Status |
|
||||||
|
|---------|----------|--------|
|
||||||
|
| `di2conf` | Read LNB config register | RxReplyTimeout (no switch connected) |
|
||||||
|
| `di2id` | Read LNB hardware ID | RxReplyTimeout |
|
||||||
|
| `di2stat` | Read LNB status flags | RxReplyTimeout |
|
||||||
|
| `di2rcs` | Read committed switch status | RxReplyTimeout |
|
||||||
|
| `di2cs` | Configure committed switch | Needs parameters |
|
||||||
|
| `di2sc` | Short circuit test | Untested |
|
||||||
|
| `send <hex>` | Raw DiSEqC packet (max 6 bytes) | Functional |
|
||||||
|
|
||||||
|
Raw DiSEqC packets use the `send` command with space-delimited hex bytes. Standard DiSEqC 1.x commands: `send E0 10 38 Fx` where the last byte selects the switch port (F0-F3 for ports 1-4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Messages
|
||||||
|
|
||||||
|
| Message | Meaning |
|
||||||
|
|---------|---------|
|
||||||
|
| `AZ MOTOR STALLED` | Obstruction preventing rotation |
|
||||||
|
| `EL MOTOR STALLED` | Obstruction preventing elevation change |
|
||||||
|
| `EL Motor Home Failure` | Requires EL recalibration via IDU menu |
|
||||||
|
| `Step to Position EL angle error: 2147483647` | INT_MAX sentinel -- motor axis uncalibrated/unhomed |
|
||||||
242
src/content/docs/reference/console-probe-cli.mdx
Normal file
242
src/content/docs/reference/console-probe-cli.mdx
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
---
|
||||||
|
title: "console-probe CLI"
|
||||||
|
description: Reference for the console-probe tool — auto-discovery, brute-force probing, and JSON reporting for embedded firmware consoles.
|
||||||
|
sidebar:
|
||||||
|
order: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The `console-probe` package is a generic embedded console scanner. It auto-detects prompts and error strings, parses help output for known commands and submenus, and brute-force probes for hidden commands. While built for the Winegard firmware, it works with any prompt-terminated serial console.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync # Installs both birdcage and console-probe
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe [connection] [discovery overrides] [probing] [output]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Discover commands via help only (safe, no brute-force)
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --discover-only --json /tmp/d.json
|
||||||
|
|
||||||
|
# Deep probe: discover + brute-force all submenus
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --deep --wordlist scripts/wordlists/winegard.txt
|
||||||
|
|
||||||
|
# Probe a single submenu
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --submenu mot
|
||||||
|
|
||||||
|
# Custom console (non-Winegard)
|
||||||
|
console-probe --port /dev/ttyACM0 --baud 9600 --prompt "U-Boot>" --error "Unknown command"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--port` | /dev/ttyUSB0 | Serial port |
|
||||||
|
| `--baud` | 115200 | Baud rate |
|
||||||
|
| `--line-ending` | cr | Line ending to send (cr, lf, crlf) |
|
||||||
|
|
||||||
|
### Discovery Overrides
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--prompt` | (auto-detect) | Override auto-detected root prompt |
|
||||||
|
| `--error` | (auto-detect) | Override auto-detected error string |
|
||||||
|
| `--help-cmd` | `?` | Command to request help |
|
||||||
|
| `--exit-cmd` | `q` | Command to exit submenu |
|
||||||
|
|
||||||
|
### Probing Options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--discover-only` | false | Discover commands via help only (no brute-force) |
|
||||||
|
| `--deep` | false | Probe all discovered submenus |
|
||||||
|
| `--submenu` | (none) | Probe a single submenu by name |
|
||||||
|
| `--timeout` | 0.5 | Per-command timeout in seconds |
|
||||||
|
| `--blocklist` | reboot,stow,def,q,Q | Comma-separated commands to never send |
|
||||||
|
| `--wordlist` | (none) | Extra candidate words file (repeatable) |
|
||||||
|
| `--bundled` | (none) | Alias for --wordlist (repeatable) |
|
||||||
|
|
||||||
|
### Output Options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--json` | (none) | Write results as JSON to FILE |
|
||||||
|
|
||||||
|
## Operating Modes
|
||||||
|
|
||||||
|
### Discover-Only Mode
|
||||||
|
|
||||||
|
With `--discover-only`, the tool:
|
||||||
|
|
||||||
|
1. Auto-detects the root prompt and error string
|
||||||
|
2. Parses root-level help output for commands and submenu names
|
||||||
|
3. Enters each submenu and queries `?` (and `man` for paginated help)
|
||||||
|
4. Records all discovered commands with descriptions and parameters
|
||||||
|
5. Writes a JSON report if `--json` is specified
|
||||||
|
|
||||||
|
This mode is completely safe -- it only sends help commands and submenu navigation.
|
||||||
|
|
||||||
|
### Standard Probe Mode
|
||||||
|
|
||||||
|
Without `--discover-only`:
|
||||||
|
|
||||||
|
1. Runs auto-discovery (same as above)
|
||||||
|
2. Generates candidate commands (built-in list + external wordlists)
|
||||||
|
3. Sends each candidate to the root menu
|
||||||
|
4. If `--deep` or `--submenu`, repeats in each submenu
|
||||||
|
5. Reports any command that produces a non-error response
|
||||||
|
|
||||||
|
### Deep Probe Mode
|
||||||
|
|
||||||
|
With `--deep`, the tool probes **all** discovered submenus:
|
||||||
|
|
||||||
|
1. Enter each submenu
|
||||||
|
2. Query help (populates `submenu_help`)
|
||||||
|
3. Brute-force probe all candidates
|
||||||
|
4. Classify results as "known" (in help) or "undiscovered" (probe-only)
|
||||||
|
5. Return to root before entering the next submenu
|
||||||
|
|
||||||
|
## Auto-Discovery Sequence
|
||||||
|
|
||||||
|
The `auto_discover()` function runs automatically unless both `--prompt` and `--error` are provided:
|
||||||
|
|
||||||
|
1. **Prompt detection:** Send a bare line ending, read the response, extract the prompt token from the last line (matches pattern `\S+[>$#]`)
|
||||||
|
|
||||||
|
2. **Error string detection:** Send a garbage command (`__xyzzy_probe__`), extract the error message from the response
|
||||||
|
|
||||||
|
3. **Help parsing:** Send `?`, parse the output for command names and submenu hints using multiple regex patterns:
|
||||||
|
- `command - description` format
|
||||||
|
- `command description` (double-space separated)
|
||||||
|
- Angle-bracket references like `Enter <a3981>`
|
||||||
|
- Bare command names on their own line
|
||||||
|
|
||||||
|
4. **Submenu registration:** Build prompt list from discovered submenus (e.g., `mot` -> `MOT>`)
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
|
||||||
|
### DeviceProfile
|
||||||
|
|
||||||
|
Everything the tool knows (or detected) about the attached console.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class DeviceProfile:
|
||||||
|
port: str = "/dev/ttyUSB0"
|
||||||
|
baud: int = 115200
|
||||||
|
root_prompt: str = "" # e.g. "TRK>"
|
||||||
|
prompts: list[str] = [] # all known prompts
|
||||||
|
error_string: str = "" # e.g. "Invalid command."
|
||||||
|
known_commands: set[str] = set() # from help output
|
||||||
|
submenus: list[str] = [] # detected submenu names
|
||||||
|
exit_cmd: str = "q"
|
||||||
|
line_ending: str = "\r"
|
||||||
|
submenu_help: dict[str, list[HelpEntry]] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HelpEntry
|
||||||
|
|
||||||
|
A single command parsed from firmware help output.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class HelpEntry:
|
||||||
|
name: str # command name (lowercase)
|
||||||
|
description: str # help description text
|
||||||
|
params: str # parameter syntax, e.g. "[<motor> [angle]]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON Report Format
|
||||||
|
|
||||||
|
The report uses `format_version: 2` and includes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"device": {
|
||||||
|
"port": "/dev/ttyUSB2",
|
||||||
|
"baud": 115200
|
||||||
|
},
|
||||||
|
"detected": {
|
||||||
|
"root_prompt": "TRK>",
|
||||||
|
"error_string": "Invalid command.",
|
||||||
|
"known_commands": ["a3981", "adc", "dvb", "..."],
|
||||||
|
"submenus": ["a3981", "adc", "dvb", "..."]
|
||||||
|
},
|
||||||
|
"menus": {
|
||||||
|
"MOT": {
|
||||||
|
"prompt": "MOT>",
|
||||||
|
"help_commands": [
|
||||||
|
{
|
||||||
|
"cmd": "a",
|
||||||
|
"description": "Go to angle [[[motor] [[+|-]angle]]]",
|
||||||
|
"params": "[[[motor] [[+|-]angle]]]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"probe_hits": [
|
||||||
|
{"cmd": "a", "response": "Angle[0] = 180.00 ..."}
|
||||||
|
],
|
||||||
|
"undiscovered": [],
|
||||||
|
"stats": {
|
||||||
|
"help_count": 25,
|
||||||
|
"probe_count": 22,
|
||||||
|
"undiscovered_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"TRK": {
|
||||||
|
"total_hits": 3,
|
||||||
|
"known": 3,
|
||||||
|
"unknown": 0,
|
||||||
|
"hits": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Report Sections
|
||||||
|
|
||||||
|
| Section | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `device` | Serial port and baud rate |
|
||||||
|
| `detected` | Auto-discovered prompt, error string, known commands, submenus |
|
||||||
|
| `menus` | Per-submenu structured data: help commands, probe hits, undiscovered commands, stats |
|
||||||
|
| `results` | Legacy results section (backward compatible): total hits, known/unknown classification |
|
||||||
|
|
||||||
|
The `menus` section is populated when `submenu_help` is available (from `--discover-only` or `--deep`). The `undiscovered` array contains commands found by brute-force probing that do not appear in the help output.
|
||||||
|
|
||||||
|
## Candidate Generation
|
||||||
|
|
||||||
|
The built-in candidate list includes:
|
||||||
|
|
||||||
|
- All single lowercase and uppercase ASCII letters
|
||||||
|
- All single digits
|
||||||
|
- ~150 generic embedded debug commands (memory access, flash, boot/system, debug, shell/OS, network, service/factory, update)
|
||||||
|
- ~200 two-letter combinations
|
||||||
|
- External wordlist files (via `--wordlist`)
|
||||||
|
|
||||||
|
Candidates are deduplicated and filtered through the blocklist before probing. The default blocklist prevents sending `reboot`, `stow`, `def`, `q`, and `Q`.
|
||||||
|
|
||||||
|
## Prompt-Terminated Reading
|
||||||
|
|
||||||
|
The serial I/O module (`serial_io.py`) uses prompt-terminated reading rather than fixed-size buffers. It reads until:
|
||||||
|
|
||||||
|
1. A known prompt string is found at the end of the response, or
|
||||||
|
2. A `PROMPT_RE` regex match (`\S+[>$#]`) appears on the last line (only if no `[` brackets on that line, to avoid matching parameter syntax), or
|
||||||
|
3. The timeout expires
|
||||||
|
|
||||||
|
This approach handles variable-length responses and avoids the common bug of truncating long firmware output.
|
||||||
|
|
||||||
|
## Parameter Placeholder Filtering
|
||||||
|
|
||||||
|
The help parser maintains a set of known parameter placeholder names (`command`, `value`, `index`, `motor`, `angle`, etc.) and filters them out to avoid treating help syntax like `help [<command>]` as a command named "command". It also checks whether a match falls inside `[...]` brackets.
|
||||||
76
src/content/docs/reference/datasheets.mdx
Normal file
76
src/content/docs/reference/datasheets.mdx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: "Datasheets"
|
||||||
|
description: Component datasheets and reference manuals for the Winegard Carryout G2 hardware — K60 MCU, A3981 motor driver, RYS352A GPS module.
|
||||||
|
sidebar:
|
||||||
|
order: 9
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
These are the key component datasheets for the hardware found in the Winegard Carryout G2. PDF downloads are available from the links below.
|
||||||
|
|
||||||
|
## MCU -- NXP MK60DN512VLQ10
|
||||||
|
|
||||||
|
The main microcontroller is an NXP Kinetis K60, an ARM Cortex-M4 with DSP instructions. This is the brain of the dish -- it runs the firmware console, motor control PID loops, DVB tuner interface, and NVS storage.
|
||||||
|
|
||||||
|
### K60 Sub-Family Data Sheet
|
||||||
|
|
||||||
|
<a href="/datasheets/K60-datasheet.pdf">Download K60 Data Sheet (PDF)</a>
|
||||||
|
|
||||||
|
- **Part:** K60P144M100SF2V2, Rev 3, June 2013
|
||||||
|
- **Contents:** Pinout, electrical specifications, package dimensions, ordering information
|
||||||
|
- **Key specs:** 144-LQFP, 100 MHz max, 512 KB flash, 128 KB RAM, 5x UART, 3x DSPI, 2x I2C, 2x 16-bit ADC, USB OTG
|
||||||
|
|
||||||
|
### K60 Reference Manual
|
||||||
|
|
||||||
|
<a href="/datasheets/K60-reference-manual.pdf">Download K60 Reference Manual (PDF)</a>
|
||||||
|
|
||||||
|
- **Part:** K60P144M100SF2V2RM
|
||||||
|
- **Contents:** ~1800 pages covering all peripheral registers, clock configuration, flash memory controller, DMA, GPIO port control, UART, SPI, I2C, ADC, USB, and more
|
||||||
|
- **Useful sections:** Chapter 11 (Port Control and Interrupts) for pin mux configuration, Chapter 28 (DSPI) for motor driver SPI interface, Chapter 45 (FTFL Flash) for flash security and programming
|
||||||
|
|
||||||
|
## Motor Driver -- Allegro A3981
|
||||||
|
|
||||||
|
Each axis (AZ and EL) is driven by an Allegro A3981 programmable stepper motor driver, controlled via SPI from the K60 MCU.
|
||||||
|
|
||||||
|
### A3981 Data Sheet
|
||||||
|
|
||||||
|
<a href="/datasheets/A3981-datasheet.pdf">Download A3981 Data Sheet (PDF)</a>
|
||||||
|
|
||||||
|
- **Manufacturer:** Allegro Microsystems
|
||||||
|
- **Contents:** Functional description, SPI register map, timing diagrams, application circuits
|
||||||
|
- **Key specs:** Up to 28V motor supply, 1/16 microstepping, automatic current decay, SPI configuration, TSSOP-28 package with exposed pad
|
||||||
|
- **ECAD files:** KiCad symbol and footprint are available in the project's `docs/A3981-ecad.*` directory
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The A3981 SPI register values for current limits are stored in NVS indices 95-98 and 110-111 as hex values (e.g., `0x0000ff30`). The A3981 datasheet describes the register bit fields these map to.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## GPS Module -- RYS352A
|
||||||
|
|
||||||
|
The PCB has a GPS module footprint, though it may be unpopulated on the Carryout G2. The firmware has GPS support (NVS indices 63-64 configure GPS thresholds) and reports "GPS Not Found" at boot.
|
||||||
|
|
||||||
|
### RYS352A Module Datasheet
|
||||||
|
|
||||||
|
<a href="/datasheets/RYS352A.pdf">Download RYS352A Data Sheet (PDF)</a>
|
||||||
|
|
||||||
|
- **Manufacturer:** REYAX
|
||||||
|
- **Contents:** Module pinout, electrical specs, NMEA output format, antenna requirements
|
||||||
|
|
||||||
|
### RYS352x PAIR Command Guide
|
||||||
|
|
||||||
|
<a href="/datasheets/RYS352x_PAIR_Command_Guide.pdf">Download PAIR Command Guide (PDF)</a>
|
||||||
|
|
||||||
|
- **Contents:** Proprietary PAIR command protocol for configuring the RYS352x GPS module -- baud rate, update rate, constellation selection, NMEA sentence enable/disable, power management
|
||||||
|
- **Relevance:** If the GPS module is populated, these commands would be used to initialize it during the boot sequence
|
||||||
|
|
||||||
|
## Markdown Conversions
|
||||||
|
|
||||||
|
Large markdown conversions of these datasheets are available in the project's `docs/` directory for text search and LLM context:
|
||||||
|
|
||||||
|
- `docs/K60-datasheet.md`
|
||||||
|
- `docs/K60-reference-manual.md`
|
||||||
|
- `docs/A3981-datasheet.md`
|
||||||
|
- `docs/RYS352x_PAIR_Command_Guide.md`
|
||||||
|
|
||||||
|
These are too large to serve as web pages but are useful for programmatic search and analysis.
|
||||||
148
src/content/docs/reference/firmware-variants.mdx
Normal file
148
src/content/docs/reference/firmware-variants.mdx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
title: "Firmware Variants"
|
||||||
|
description: Comparison of all five known Winegard dish firmware variants — connection types, baud rates, motor protocols, and key differences.
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Five Winegard dish variants have been documented by Gabe Emerson (KL1FI) / saveitforparts and cdavidson0522. Each runs different firmware with its own serial protocol, motor commands, and boot sequence.
|
||||||
|
|
||||||
|
## Variant Comparison Table
|
||||||
|
|
||||||
|
| Detail | HAL 0.0.00 | HAL 2.05.003 | Trav'ler Pro | Carryout | Carryout G2 |
|
||||||
|
|--------|-----------|-------------|-------------|---------|------------|
|
||||||
|
| **Repo** | Travler_Rotor | Trav-ler-Rotor-For-HAL-2.05 | Travler-Pro-Rotor | Carryout-Rotor | winegard-sky-scan |
|
||||||
|
| **Connection** | RS-485 / RJ-25 | RS-485 / RJ-25 | USB A-to-A (`ttyACM0`) | RS-485 / RJ-25 | RS-422 / RJ-12 6P6C |
|
||||||
|
| **Baud rate** | 57600 | 57600 | 57600 | 57600 | 115200 |
|
||||||
|
| **Motor submenu** | `mot` | `motor` | `odu` then `mot` | N/A (`target` + `g`) | `mot` |
|
||||||
|
| **Motor control** | `a <id> <deg>` | `a <id> <deg>` | `a <id> <deg>` | `g <az> <el>` only | `a <id> <deg>` |
|
||||||
|
| **Search kill** | `os` -> `kill Search` | `ngsearch` -> `s` -> `q` | `os` -> `kill Search` | N/A | NVS 20 (permanent disable) |
|
||||||
|
| **Boot signal** | `NoGPS` | `NoGPS` or `No LNB Voltage` | undocumented | N/A | `Boot Complete` then `Loc Startup: IDU NOT Present` |
|
||||||
|
| **Min elevation** | 15 deg | 15 deg | 12 deg | 22 deg | 18 deg |
|
||||||
|
| **Max elevation** | 90 deg | 90 deg | 75 deg (hardware cap) | 73 deg (NVS 102 override) | 65 deg |
|
||||||
|
| **Position query** | `a` -> `AZ = / EL =` | `a` -> `AZ = / EL =` | `a` -> `AZ = / EL =` | raw ints / 100 | `a` -> floats |
|
||||||
|
| **Tested model** | LG-2112 | LG-2112 | SK2DISH | 2003 Carryout | Carryout G2 |
|
||||||
|
| **HAL version** | 0.0.00 | 2.05.003 | unknown | 1.00.065 | 02.02.48 |
|
||||||
|
| **Prompt char** | `>` (likely) | `>` (likely) | undocumented | undocumented | `TRK>` / `MOT>` / `NVS>` |
|
||||||
|
| **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 |
|
||||||
|
| **Motor driver** | unknown | unknown | unknown | unknown | 2x Allegro A3981 |
|
||||||
|
|
||||||
|
## Per-Variant Details
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="HAL 0.0.00">
|
||||||
|
|
||||||
|
The original Trav'ler firmware found on older LG-2112 units.
|
||||||
|
|
||||||
|
- **Connection:** RS-485 half-duplex via RJ-25 at 57600 baud
|
||||||
|
- **Motor submenu:** `mot`
|
||||||
|
- **Motor commands:** `a <id> <deg>` for individual motor control (0=AZ, 1=EL)
|
||||||
|
- **Search kill:** Enter `os` submenu, then `kill Search` to terminate the satellite search task
|
||||||
|
- **Boot signal:** `NoGPS` indicates calibration is complete
|
||||||
|
- **Position format:** `AZ = <val> EL = <val> SK = <val>`
|
||||||
|
- **Elevation range:** 15 -- 90 degrees
|
||||||
|
- **Source:** [github.com/saveitforparts/Travler_Rotor](https://github.com/saveitforparts/Travler_Rotor)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="HAL 2.05">
|
||||||
|
|
||||||
|
Updated Trav'ler firmware with a different search kill sequence.
|
||||||
|
|
||||||
|
- **Connection:** RS-485 half-duplex via RJ-25 at 57600 baud
|
||||||
|
- **Motor submenu:** `motor` (note: different from HAL 0.0.00's `mot`)
|
||||||
|
- **Motor commands:** `a <id> <deg>` for individual motor control
|
||||||
|
- **Search kill:** `ngsearch` -> `s` -> `q` (three-step sequence)
|
||||||
|
- **Boot signal:** `NoGPS` or `No LNB Voltage`
|
||||||
|
- **Position format:** `AZ = <val> EL = <val> SK = <val>`
|
||||||
|
- **Elevation range:** 15 -- 90 degrees
|
||||||
|
- **Known issue:** Leap-frog elevation bug (modifies `target_az` instead of `target_el` in lines 98-105)
|
||||||
|
- **Source:** [github.com/saveitforparts/Trav-ler-Rotor-For-HAL-2.05](https://github.com/saveitforparts/Trav-ler-Rotor-For-HAL-2.05)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Trav'ler Pro">
|
||||||
|
|
||||||
|
The Pro model has its own MCU in the IDU, requiring a tunnel command to reach the ODU motor controller.
|
||||||
|
|
||||||
|
- **Connection:** USB A-to-A (appears as `ttyACM0`)
|
||||||
|
- **Motor submenu:** Must first send `odu` to tunnel to the outdoor unit, then `mot`
|
||||||
|
- **Motor commands:** `a <id> <deg>` once inside the ODU motor submenu
|
||||||
|
- **Search kill:** `os` -> `kill Search` (same as HAL 0.0.00)
|
||||||
|
- **Max elevation:** 75 degrees (hardware cap, not firmware-configurable)
|
||||||
|
- **Known issue:** Same leap-frog elevation bug as HAL 2.05 (copy-pasted code)
|
||||||
|
- **Source:** [github.com/saveitforparts/Travler-Pro-Rotor](https://github.com/saveitforparts/Travler-Pro-Rotor)
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The regular Trav'ler's IDU is a dumb RS-485 passthrough. The Pro's IDU has its own MCU, so you must tunnel through it with the `odu` command before motor commands reach the ODU.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Carryout">
|
||||||
|
|
||||||
|
The 2003 Carryout uses a completely different motor addressing scheme.
|
||||||
|
|
||||||
|
- **Connection:** RS-485 half-duplex via RJ-25 at 57600 baud
|
||||||
|
- **Motor control:** No individual motor addressing. Uses `target` to enter targeting mode, then `g <az> <el>` for combined moves
|
||||||
|
- **Search kill:** N/A (no documented search kill mechanism)
|
||||||
|
- **Min elevation:** 22 degrees (firmware-enforced)
|
||||||
|
- **Max elevation:** 73 degrees (firmware default, overridable via NVS 102)
|
||||||
|
- **Position format:** Raw integers divided by 100
|
||||||
|
- **No limit switches:** Uses motor stalling to detect mechanical boundaries (audible grinding during calibration)
|
||||||
|
- **Cannot query initial position** before first move
|
||||||
|
- **DIP switches:** All switches to off (up) may disable search mode, but behavior varies by unit
|
||||||
|
- **Source:** [github.com/saveitforparts/Carryout-Rotor](https://github.com/saveitforparts/Carryout-Rotor)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Carryout G2">
|
||||||
|
|
||||||
|
The most fully reverse-engineered variant. Over 100 firmware commands mapped across 12 submenus.
|
||||||
|
|
||||||
|
- **Connection:** RS-422 full-duplex via RJ-12 6P6C at 115200 baud
|
||||||
|
- **Motor submenu:** `mot`
|
||||||
|
- **Motor commands:** `a <id> <deg>` (protocol-compatible with the Trav'ler family)
|
||||||
|
- **Search disable:** NVS index 20 set to TRUE (permanent, one-time configuration)
|
||||||
|
- **Boot sequence:** Bootloader v1.01 -> SPI1 init (A3981 motor drivers) -> SPI2 init (BCM4515 DVB tuner) -> NVS load -> EL home -> AZ home -> `TRK>` prompt
|
||||||
|
- **Position format:** `Angle[0] = <az>` / `Angle[1] = <el>` (floats)
|
||||||
|
- **Elevation range:** 18 -- 65 degrees
|
||||||
|
- **Cable wrap:** -423.33 to +23.33 degrees from home (total 446.66 degrees)
|
||||||
|
- **MCU:** NXP MK60DN512VLQ10 (Kinetis K60, Cortex-M4, 96 MHz, 512 KB flash, 128 KB RAM)
|
||||||
|
- **Motor drivers:** 2x Allegro A3981 (SPI, 1/16 microstep, AUTO mode)
|
||||||
|
- **DVB tuner:** BCM4515 (ID 0x4515, Rev B0, firmware v113.37)
|
||||||
|
- **Firmware:** Version 02.02.48 (Copyright 2013 - Winegard Company)
|
||||||
|
- **Homing:** Explicit `h <id>` motor home command. Uses stall detection.
|
||||||
|
- **DVB/RSSI:** Full signal strength measurement through BCM4515 tuner
|
||||||
|
- **Source:** [github.com/cdavidson0522/winegard-sky-scan](https://github.com/cdavidson0522/winegard-sky-scan)
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
When NVS 20 = TRUE (tracker disabled), homing is skipped entirely on boot. Motors stay uncalibrated and AZ position reads as INT_MAX (2147483647). You must run `h *` in the MOT submenu to home both axes before sending position commands.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Key Variant Differences
|
||||||
|
|
||||||
|
- **Trav'ler Pro `odu` command:** The Pro's IDU has its own MCU. You must first tunnel to the ODU with `odu` before entering the motor submenu. The regular Trav'ler's IDU is a dumb RS-485 passthrough.
|
||||||
|
|
||||||
|
- **Carryout uses `g` not `a`:** The 2003 Carryout has no individual motor addressing. It uses `target` to enter targeting mode, then `g <az> <el>` for combined moves. It also cannot query its initial position.
|
||||||
|
|
||||||
|
- **Carryout has no limit switches:** Uses motor stalling to detect mechanical boundaries (audible grinding during calibration).
|
||||||
|
|
||||||
|
- **Pro has the same leap-frog bug** as the regular Trav'ler (copy-pasted code). Fixed in this project's `leapfrog.py`.
|
||||||
|
|
||||||
|
- **NVS `d` command** dumps all NVS values. Confirmed on Pro and Carryout G2; likely available on all variants.
|
||||||
|
|
||||||
|
- **Carryout G2 uses `a` not `g`:** Unlike the 2003 Carryout, the G2 uses standard `a <id> <deg>` motor addressing and the `mot` submenu -- protocol-compatible with the Trav'ler family.
|
||||||
|
|
||||||
|
- **Carryout G2 is RS-422 full-duplex:** Separate TX/RX pairs at 115200 baud via RJ-12 6P6C, vs. RS-485 half-duplex at 57600 on the Trav'ler variants.
|
||||||
|
|
||||||
|
- **Polarity matters on the G2:** A/B (or +/-) labeling is not standardized. Garbled data at the correct baud rate means the RX pair polarity is swapped. Silent failure (no response from dish) means the TX pair polarity is swapped.
|
||||||
|
|
||||||
|
- **Carryout G2 position format differs:** Position query `a` in `MOT>` returns `Angle[0] = 180.00` / `Angle[1] = 45.00` instead of the `AZ = / EL =` format used by HAL 0.0.00 and HAL 2.05.
|
||||||
|
|
||||||
|
- **Carryout G2 has `h <id>` homing:** Explicit motor home-to-reference command. Not documented on other variants.
|
||||||
|
|
||||||
|
- **Carryout G2 has DVB/RSSI:** BCM4515 tuner provides signal strength measurement, DVB analysis, DiSEqC 2.x control, and a built-in radio telescope mode (`azscanwxp`).
|
||||||
120
src/content/docs/reference/gpio-pinmap.mdx
Normal file
120
src/content/docs/reference/gpio-pinmap.mdx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
title: "GPIO Pin Map"
|
||||||
|
description: K60 MCU GPIO functional pin map for the Winegard Carryout G2 — SPI buses, UART, DIP switches, and unidentified outputs cross-referenced from live queries and datasheets.
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Cross-referenced from live `gpio dir`/`gpio regs` queries (2026-02-13), K60 datasheet pin mux table (MK60DN512VLQ10, 144-LQFP), boot log peripheral init, and A3981 datasheet.
|
||||||
|
|
||||||
|
## SPI1 -- A3981 Stepper Motor Drivers (4 MHz, mode 0x03)
|
||||||
|
|
||||||
|
The two A3981 stepper motor driver ICs are controlled via SPI1 at 4 MHz. SPI mode 0x03 means CPOL=1, CPHA=1.
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | Alt | Function | Dir | State | Notes |
|
||||||
|
|---------|------|-----|----------|-----|-------|-------|
|
||||||
|
| PTE0 | E0 | ALT2 | SPI1_PCS1 | OUT | 1 | A3981 #2 chip select (EL motor) |
|
||||||
|
| PTE1 | E1 | ALT2 | SPI1_SOUT | (periph) | 1 | MOSI -- MCU to A3981 |
|
||||||
|
| PTE2 | E2 | ALT2 | SPI1_SCK | (periph) | 1 | SPI clock |
|
||||||
|
| PTE3 | E3 | ALT2 | SPI1_SIN | (periph) | 0 | MISO -- A3981 to MCU |
|
||||||
|
| PTE4 | E4 | ALT2 | SPI1_PCS0 | IN* | 1 | A3981 #1 chip select (AZ motor) |
|
||||||
|
| PTE5 | E5 | ALT2 | SPI1_PCS2 | OUT | 1 | Possibly A3981 RESET or enable |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
PTE4 shows INPUT in the GPIO dir register, but this is irrelevant when muxed to the SPI peripheral. The SPI controller manages chip select assertion/deassertion directly.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## SPI2 -- BCM4515 DVB-S2 Tuner (6.857 MHz, mode 0x03)
|
||||||
|
|
||||||
|
The BCM4515 DVB-S2 tuner communicates via SPI2. The clock frequency is 48 MHz / 7 = 6.857 MHz.
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | Alt | Function | Dir | State | Notes |
|
||||||
|
|---------|------|-----|----------|-----|-------|-------|
|
||||||
|
| PTD11 | D11 | ALT2 | SPI2_PCS0 | OUT | 1 | BCM4515 chip select |
|
||||||
|
| PTD12 | D12 | ALT2 | SPI2_SCK | IN* | 1 | SPI clock |
|
||||||
|
| PTD13 | D13 | ALT2 | SPI2_SOUT | IN* | 1 | MOSI -- MCU to BCM4515 |
|
||||||
|
| PTD14 | D14 | ALT2 | SPI2_SIN | -- | 0 | MISO -- BCM4515 to MCU |
|
||||||
|
| PTD15 | D15 | ALT2 | SPI2_PCS1 | -- | 0 | Secondary chip select (unused?) |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
GPIO dir register values are not meaningful for peripheral-muxed pins. The SPI controller drives these pins regardless of the GPIO direction setting.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## UART4 -- RS-422 Console (115200 baud)
|
||||||
|
|
||||||
|
The console serial port used for all firmware interaction.
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | Alt | Function | Dir | State | Notes |
|
||||||
|
|---------|------|-----|----------|-----|-------|-------|
|
||||||
|
| PTE24 | E24 | ALT3 | UART4_TX | OUT | 1 | Console TX (to computer RX pair) |
|
||||||
|
| PTE25 | E25 | ALT3 | UART4_RX | IN | 1 | Console RX (from computer TX pair) |
|
||||||
|
| PTE26 | E26 | ALT3 | UART4_CTS | IN | 1 | Hardware flow control (idle high) |
|
||||||
|
| PTE27 | E27 | -- | GPIO | IN | 1 | Unknown (RTS? or pullup) |
|
||||||
|
| PTE28 | E28 | -- | GPIO | IN | 1 | Unknown |
|
||||||
|
|
||||||
|
## DIP Switch GPIOs
|
||||||
|
|
||||||
|
The `dipswitch` command reads the raw value `val:ffffff01` (all OFF/up), which maps to `app_dipswitch:101` (DISH 110+119+129 degrees W).
|
||||||
|
|
||||||
|
Exact GPIO pins are TBD -- likely Port A or Port C inputs with internal pullups. The 0xffffff01 raw value suggests a 32-bit register read where bits 1-24 are all high (pullup, switches open) and bit 0 is high (LSB).
|
||||||
|
|
||||||
|
## A3981 Diagnostic Pins
|
||||||
|
|
||||||
|
The `a3981 diag` command reads fault status from two GPIO pins (one per motor driver). Both read "OK" when motors are healthy. The A3981 DIAG output is active-low open-drain, pulled high when no fault is present.
|
||||||
|
|
||||||
|
Exact GPIO pins are TBD -- likely on Port A or Port E near the SPI1 bus.
|
||||||
|
|
||||||
|
## Unidentified High-State Outputs
|
||||||
|
|
||||||
|
These GPIO pins were observed in the HIGH state (logic 1) during the `gpio regs` dump. Their functions are inferred from context and K60 pin mux options.
|
||||||
|
|
||||||
|
| GPIO | Dir | State | Likely Function |
|
||||||
|
|------|-----|-------|-----------------|
|
||||||
|
| D10 | OUT | 1 | BCM4515 reset or power enable |
|
||||||
|
| B0-B3 | -- | 1 | SPI0 or I2C bus (B0-B3 cluster) |
|
||||||
|
| B11 | -- | 1 | Status LED or peripheral enable |
|
||||||
|
| C10-C13 | -- | 1 | Contiguous block -- possibly bus interface |
|
||||||
|
| C18 | -- | 1 | LNB voltage control or relay |
|
||||||
|
|
||||||
|
## Full GPIO Register Summary
|
||||||
|
|
||||||
|
From the `gpio regs` command, 101 pins are enumerated across 5 ports:
|
||||||
|
|
||||||
|
| Port | Pins Enumerated | Count | High Pins (=1) |
|
||||||
|
|------|----------------|-------|----------------|
|
||||||
|
| A | A0-A19, A24-A29 | 26 | A1, A3, A4, A5, A15, A16, A25-A29 |
|
||||||
|
| B | B0-B11, B16-B23 | 20 | B0, B1, B2, B3, B11 |
|
||||||
|
| C | C0-C19 | 20 | C10, C11, C12, C13, C18 |
|
||||||
|
| D | D0-D15 | 16 | D11, D12, D13 |
|
||||||
|
| E | E0-E12, E24-E29 | 19 | E0, E1, E2, E4, E5, E7, E9-E12, E24-E28 |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Pins A20-A23 and B12-B15 are not enumerated (reserved or unbonded on the 144-LQFP package). E29 shows "Unknown bit E29" in the firmware output -- the pin is defined in hardware but not assigned a function.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## USB Port Pins (Potentially Accessible)
|
||||||
|
|
||||||
|
The K60 has dedicated USB pins that cannot be muxed to 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, proving 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.
|
||||||
|
|
||||||
|
## SWD / Debug Interface Pins
|
||||||
|
|
||||||
|
For firmware extraction via SWD debug probe:
|
||||||
|
|
||||||
|
| 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 |
|
||||||
133
src/content/docs/reference/hardware-specs.mdx
Normal file
133
src/content/docs/reference/hardware-specs.mdx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
title: "Hardware Specs"
|
||||||
|
description: Physical specifications for the Winegard Trav'ler SK-1000 and Carryout G2 — dimensions, power, serial connector pinouts, adapter chains, and physical setup.
|
||||||
|
sidebar:
|
||||||
|
order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
## SK-1000 Specifications
|
||||||
|
|
||||||
|
| Spec | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Weight | 45 lbs |
|
||||||
|
| Stow dimensions | 10"H x 42"L x 26"W |
|
||||||
|
| Stow height | 9.75" |
|
||||||
|
| Max deployed height | 37" above mount |
|
||||||
|
| Arm reach | 32.5" from base center |
|
||||||
|
| Dish size | ~33" x 23" (Ku-band reflector) |
|
||||||
|
| Azimuth range | 0-455 deg (before cable wrap) |
|
||||||
|
| Power input | 120VAC -> 12VDC (RP-SK87 supply) |
|
||||||
|
| LNB bias | 12-18VDC via coax |
|
||||||
|
| Motor type | Stepper with gearing |
|
||||||
|
| Satellites (DISH) | 110, 119, 129 (61.5 manual only) |
|
||||||
|
| Satellites (Bell) | 82, 91 |
|
||||||
|
|
||||||
|
## Carryout G2 Specifications
|
||||||
|
|
||||||
|
| Spec | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Dish diameter | ~12" (12-IN G2) |
|
||||||
|
| Connection | RS-422 full-duplex / RJ-12 6P6C |
|
||||||
|
| Baud rate | 115200 8N1 |
|
||||||
|
| MCU | NXP MK60DN512VLQ10 (Cortex-M4, 96 MHz) |
|
||||||
|
| Flash | 512 KB (64 KB bootloader + 448 KB application) |
|
||||||
|
| RAM | 128 KB |
|
||||||
|
| Motor drivers | 2x Allegro A3981 (SPI, 1/16 microstep) |
|
||||||
|
| DVB tuner | BCM4515 (ID 0x4515, Rev B0, FW v113.37) |
|
||||||
|
| Firmware | 02.02.48 (Copyright 2013 - Winegard Company) |
|
||||||
|
| Bootloader | v1.01 |
|
||||||
|
| AZ steps/rev | 40000 |
|
||||||
|
| EL steps/rev | 24960 |
|
||||||
|
| AZ max velocity | 65.0 deg/s |
|
||||||
|
| EL max velocity | 45.0 deg/s |
|
||||||
|
| Elevation range | 18-65 deg (firmware enforced) |
|
||||||
|
| Cable wrap range | -423.33 to +23.33 deg (total 446.66 deg) |
|
||||||
|
| Min elevation | 18 deg |
|
||||||
|
| Max elevation | 65 deg |
|
||||||
|
| System ID | TWELINCH |
|
||||||
|
| Antenna ID | 12-IN G2 |
|
||||||
|
|
||||||
|
## Serial Connector Pinout
|
||||||
|
|
||||||
|
The physical connector is an RJ-25 (6P6C) on the Trav'ler or RJ-12 (6P6C) on the G2 -- same form factor, same 6-pin modular jack.
|
||||||
|
|
||||||
|
### Trav'ler Pinout (RJ-25, bottom view, clip up)
|
||||||
|
|
||||||
|
| Pin | Label | RS-485 use | RS-422 use |
|
||||||
|
|-----|-------|-----------|-----------|
|
||||||
|
| 1 | GND | Ground | Ground |
|
||||||
|
| 2 | T/R- | Shared data- | TX- (computer to dish) |
|
||||||
|
| 3 | T/R+ | Shared data+ | TX+ (computer to dish) |
|
||||||
|
| 4 | RXD- | (unused in half-duplex) | RX- (dish to computer) |
|
||||||
|
| 5 | RXD+ | (unused in half-duplex) | RX+ (dish to computer) |
|
||||||
|
| 6 | N/C | Not connected | Not connected |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The Trav'ler's RJ-25 connector exposes **both** a half-duplex pair (pins 2-3, labeled T/R) **and** a dedicated receive pair (pins 4-5, labeled RXD). Gabe's code uses only the half-duplex pair via an RS-485 adapter. The same connector may support RS-422 full-duplex mode depending on the firmware.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Carryout G2 Pinout (RJ-12, clip away)
|
||||||
|
|
||||||
|
| Pin | Wire Color (Davidson) | Wire Color (confirmed) | RS-422 Function |
|
||||||
|
|-----|----------------------|----------------------|-----------------|
|
||||||
|
| 1 | White | Orange/White | GND (PE) |
|
||||||
|
| 2 | Red | Orange | TX+ (TA) -- computer to dish |
|
||||||
|
| 3 | Black | Green/White | TX- (TB) -- computer to dish |
|
||||||
|
| 4 | Yellow | Blue | RX+ (RA) -- dish to computer |
|
||||||
|
| 5 | Green | Blue/White | RX- (RB) -- dish to computer |
|
||||||
|
| 6 | Blue | Green | Not connected |
|
||||||
|
|
||||||
|
<Aside type="caution" title="Polarity is critical">
|
||||||
|
Wire colors vary by cable manufacturer. The "confirmed" column is from a standard 6P6C flat cable tested 2026-02-12. Always verify with a multimeter before connecting.
|
||||||
|
|
||||||
|
Swapping +/- on the **RX pair** produces garbled data at the correct baud rate (systematic bit inversion, not random noise). Swapping +/- on the **TX pair** causes silent failure (dish does not respond because it cannot decode the inverted framing).
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### IDU/ODU Cable Wiring (if cut)
|
||||||
|
|
||||||
|
Top row: Green, Yellow, Orange. Bottom row: Red, Brown, Black.
|
||||||
|
|
||||||
|
## Adapter Chain by Variant
|
||||||
|
|
||||||
|
| Variant | Adapter | Wires Used |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| Trav'ler (Gabe's setup) | USB -> RS232 -> RS485 (DTECH) | Pins 2-3 only (half-duplex) |
|
||||||
|
| Carryout G2 (Davidson) | USB -> RS422 (5V TTL) | Pins 2-5 (full-duplex) |
|
||||||
|
| Carryout G2 (confirmed) | DSD TECH SH-U11 USB -> RS422 (FTDI FT232R) | Pins 1-5 (full-duplex + GND) |
|
||||||
|
| Carryout G2 (ESP32) | ESP32 UART2 -> RS422 module (DIYables) | Pins 2-5 (full-duplex) |
|
||||||
|
|
||||||
|
## Power
|
||||||
|
|
||||||
|
120VAC input to RP-SK87 power supply, outputs 12VDC to IDU. Internal coax carries 12-18VDC bias for the LNB.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Do not connect 5V equipment (SDR LNAs, etc.) to the coax without bypassing the power injector. The LNB bias is 12-18VDC.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Physical Setup
|
||||||
|
|
||||||
|
- Base marked with arrows and "BACK" at 0/360 deg (North)
|
||||||
|
- Align "BACK" with true North for accurate tracking
|
||||||
|
- Gpredict rotor config: 127.0.0.1:4533, 0->180->360 mode, min EL 15, max EL 90
|
||||||
|
- No obstructions taller than 8" within 32.5" of base center
|
||||||
|
|
||||||
|
## Calibration
|
||||||
|
|
||||||
|
On power-up, the dish performs calibration movements to establish position and cable wrap limits (~10-15 minutes on Carryout, shorter on Trav'ler). After calibration, firmware automatically starts a TV satellite search -- the init sequence kills this.
|
||||||
|
|
||||||
|
The Carryout uses motor stalling (not limit switches) to detect mechanical boundaries. Expect audible grinding during calibration.
|
||||||
|
|
||||||
|
**EL recalibration (via IDU buttons):** POWER -> ENTER (hold 2s) -> User Menu -> INSTALLATION -> Calibrate EL -> confirm hard stop position.
|
||||||
|
|
||||||
|
## Emergency Manual Stow
|
||||||
|
|
||||||
|
<Aside type="danger" title="Last resort only">
|
||||||
|
Emergency manual stow can damage the motor if done improperly. Only use this when all other options have failed.
|
||||||
|
|
||||||
|
1. Use a 5/16" socket + 6" extension into the auxiliary drive hole
|
||||||
|
2. Turn clockwise slowly
|
||||||
|
3. Ensure the arm faces the "rear" label before lowering
|
||||||
|
4. Improper execution can strip gears or damage the motor
|
||||||
|
</Aside>
|
||||||
199
src/content/docs/reference/nvs-settings.mdx
Normal file
199
src/content/docs/reference/nvs-settings.mdx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
title: "NVS Settings"
|
||||||
|
description: Non-Volatile Storage index table for the Winegard Carryout G2 firmware — all configurable parameters with defaults and descriptions.
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The NVS (Non-Volatile Storage) subsystem stores operational parameters for the Carryout G2 firmware. Access it from the root menu with `nvs`, then use `d` to dump all values or `e <idx>` to read individual entries.
|
||||||
|
|
||||||
|
<Aside type="caution" title="Write with care">
|
||||||
|
`e <idx> <value>` writes values to the in-memory copy immediately. Changes are **not** persisted to flash until you send `s` (save). Any unrecognized input in the NVS submenu is treated as a sequential index read (no error string), which is harmless but can be confusing.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## NVS Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `d` | Dump all NVS values (name / current / saved / default) |
|
||||||
|
| `d <idx>` | Dump single value with details |
|
||||||
|
| `e <idx>` | Read NVS value at index |
|
||||||
|
| `e <idx> <v>` | Write NVS value at index (not saved until `s`) |
|
||||||
|
| `s` | Save pending changes to flash |
|
||||||
|
|
||||||
|
## Key Parameters for Satellite Tracking
|
||||||
|
|
||||||
|
These are the settings most relevant to repurposing the dish for amateur radio tracking.
|
||||||
|
|
||||||
|
| Index | Setting | Default | Notes |
|
||||||
|
|------:|---------|---------|-------|
|
||||||
|
| 20 | Disable Tracker Proc? | FALSE | Set TRUE to prevent TV satellite search on boot |
|
||||||
|
| 38 | Sleep Mode Timer Secs | 420 | 7 minutes before sleep |
|
||||||
|
| 80 | AZ Max Vel | 65.00 | deg/s azimuth max velocity |
|
||||||
|
| 85 | EL Max Vel | 45.00 | deg/s elevation max velocity |
|
||||||
|
| 101 | Minimum Elevation Angle | 18.00 | Firmware floor (degrees) |
|
||||||
|
| 102 | Maximum Elevation Angle | 65.00 | Firmware ceiling (degrees) |
|
||||||
|
| 103 | Elevation Home Angle | 65.00 | EL position after homing |
|
||||||
|
| 128-133 | AZ/EL PID Gains | varies | Kp/Kv/Ki tuning parameters |
|
||||||
|
|
||||||
|
## Complete NVS Dump
|
||||||
|
|
||||||
|
Full dump from firmware 02.02.48, captured 2026-02-12.
|
||||||
|
|
||||||
|
### Debug / Console
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 0 | Log ID's | 0x00000007 | 0x00000007 | 0x00000007 | |
|
||||||
|
| 1 | Log Device | 0x00000001 | 0x00000001 | 0x00000001 | |
|
||||||
|
| 2 | Debug 2nd Console Port | 0 | 0 | 0 | Might enable USB console |
|
||||||
|
| 3 | Debug 2nd Packet Port | 0 | 0 | 0 | |
|
||||||
|
| 4 | Debug Port Connection | 0 | 0 | 0 | Might enable USB console |
|
||||||
|
|
||||||
|
### Conical / Tracking Parameters
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 16 | Pitch Deadband | 0.00 | 0.00 | 0.00 | |
|
||||||
|
| 17 | Roll Deadband | 0.00 | 0.00 | 0.00 | |
|
||||||
|
| 18 | Yaw Deadband | 0.00 | 0.00 | 0.00 | |
|
||||||
|
| 20 | Disable Tracker Proc? | TRUE | TRUE | FALSE | **MODIFIED** -- prevents TV search on boot |
|
||||||
|
| 21 | Tracker Proc Run Mode | 0 | 0 | 0 | |
|
||||||
|
| 22 | Conical Alpha Az | 200 | 200 | 200 | |
|
||||||
|
| 23 | Conical Alpha El | 200 | 200 | 200 | |
|
||||||
|
| 24 | Conical Radius | 1.00 | 1.00 | 1.00 | |
|
||||||
|
| 25 | Conical Count Max | 20 | 20 | 20 | |
|
||||||
|
| 26 | Conical Test Drift | +0 | +0 | +0 | |
|
||||||
|
| 27 | Circle RPM | 120 | 120 | 120 | |
|
||||||
|
| 28 | Circle Pts/Rev | 6 | 6 | 6 | |
|
||||||
|
| 32 | Conical Az Clamp | 8.00 | 8.00 | 8.00 | |
|
||||||
|
| 33 | Conical El Clamp | 8.00 | 8.00 | 8.00 | |
|
||||||
|
| 35 | Motor Pts/Rev | 72 | 72 | 72 | |
|
||||||
|
| 36 | Circle Az Radius | 1.00 | 1.00 | 1.00 | |
|
||||||
|
| 37 | Circle El Radius | 1.00 | 1.00 | 1.00 | |
|
||||||
|
|
||||||
|
### Sleep / Scan
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 38 | Sleep Mode Timer Secs | 420 | 420 | 420 | 7 minutes |
|
||||||
|
| 40 | Motor Type | 0 | 0 | 0 | |
|
||||||
|
| 41 | Satellite Scan Velocity | 55.00 | 55.00 | 55.00 | deg/s during TV search |
|
||||||
|
| 48 | Motor Spiral Velocity | 55.00 | 55.00 | 55.00 | |
|
||||||
|
| 49 | Motor Gear Ratio | 0x00000000 | 0x00000000 | 0x00000000 | |
|
||||||
|
|
||||||
|
### GPS
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 63 | GPS Heading Threshold | 1.00 | 1.00 | 1.00 | |
|
||||||
|
| 64 | GPS Moving Threshold | 5.00 MPH | 5.00 MPH | 5.00 MPH | |
|
||||||
|
|
||||||
|
### Signal Processing
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 66 | Spiral Signal In A Row Min | +3 | +3 | +3 | |
|
||||||
|
| 67 | Spiral Signal In A Row Max | +20 | +20 | +20 | |
|
||||||
|
| 68 | Signal Odd to Even Offset | +0 | +0 | +0 | |
|
||||||
|
| 69 | Signal Offset | 80 | 80 | 80 | |
|
||||||
|
| 70 | Signal Baseline Angle | 65.00 | 65.00 | 65.00 | |
|
||||||
|
| 71 | Signal Re-Peak Degrade Percent | 25 | 25 | 25 | |
|
||||||
|
|
||||||
|
### Gyro / IMU
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 72 | Gyro Sensitivity | +1110 | +1110 | +1110 | |
|
||||||
|
| 73 | Gyro Filter Size | +1 | +1 | +1 | |
|
||||||
|
| 74 | Gyro Calib Readings | 100 | 100 | 100 | |
|
||||||
|
| 75 | Gyro Mount Type | 1 | 1 | 1 | |
|
||||||
|
| 76 | Gyro Velocity Offset | 4 | 4 | 4 | |
|
||||||
|
| 77 | Gyro Max Accel | 600 | 600 | 600 | |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
NVS indices 72-77 configure gyro parameters, but the G2 boot log says nothing about a gyro. The G2 may share a PCB design with a larger model that includes a MEMS gyro -- the footprint may be unpopulated (DNP).
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Motor Dynamics
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 80 | AZ Max Vel | 65.00 | 65.00 | 65.00 | deg/s |
|
||||||
|
| 81 | AZ Max Accel | 400.00 | 400.00 | 400.00 | deg/s^2 |
|
||||||
|
| 82 | AZ Home Velocity | 55.00 | 55.00 | 55.00 | deg/s |
|
||||||
|
| 83 | AZ Steps/Rev | 40000 | 40000 | 40000 | Stepper steps per full rotation |
|
||||||
|
| 84 | AZ Direction | +1 | +1 | +1 | |
|
||||||
|
| 85 | EL Max Vel | 45.00 | 45.00 | 45.00 | deg/s |
|
||||||
|
| 86 | EL Max Accel | 400.00 | 400.00 | 400.00 | deg/s^2 |
|
||||||
|
| 87 | EL Home Velocity | 45.00 | 45.00 | 45.00 | deg/s |
|
||||||
|
| 88 | EL Steps/Rev | 24960 | 24960 | 24960 | Stepper steps per full EL rotation |
|
||||||
|
| 89 | EL Direction | +1 | +1 | +1 | |
|
||||||
|
|
||||||
|
### Motor Current Limits
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 95 | AZ Low current limit | 0x0000ff0c | 0x0000ff0c | 0x0000ff0c | A3981 register value |
|
||||||
|
| 96 | AZ High current limit | 0x0000ff30 | 0x0000ff30 | 0x0000ff30 | A3981 register value |
|
||||||
|
| 97 | EL Low current limit | 0x0000ff0c | 0x0000ff0c | 0x0000ff0c | A3981 register value |
|
||||||
|
| 98 | EL High current limit | 0x0000ff40 | 0x0000ff40 | 0x0000ff40 | A3981 register value |
|
||||||
|
|
||||||
|
### Elevation Limits
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 101 | Minimum Elevation Angle | 18.00 | 18.00 | 18.00 | Firmware floor (degrees) |
|
||||||
|
| 102 | Maximum Elevation Angle | 65.00 | 65.00 | 65.00 | Firmware ceiling (degrees) |
|
||||||
|
| 103 | Elevation Home Angle | 65.00 | 65.00 | 65.00 | EL position after homing |
|
||||||
|
|
||||||
|
### Stall Detection
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 106 | Az Stall Detect | 78 | 78 | 78 | Threshold |
|
||||||
|
| 107 | El Stall Detect | 75 | 75 | 75 | Threshold |
|
||||||
|
| 108 | Az Stall Samples | 100 | 100 | 100 | |
|
||||||
|
| 109 | El Stall Samples | 100 | 100 | 100 | |
|
||||||
|
| 110 | EL Home Current Limit | 0x0000ff28 | 0x0000ff28 | 0x0000ff28 | A3981 register value |
|
||||||
|
| 111 | AZ Home Current Limit | 0x0000ff40 | 0x0000ff40 | 0x0000ff40 | A3981 register value |
|
||||||
|
|
||||||
|
### Dipswitch / Config
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 112 | Disable Dipswitch? | FALSE | FALSE | FALSE | Override physical DIP switches |
|
||||||
|
| 113 | Dipswitch Value | 101 | 101 | 101 | DirecTV config (ignored when tracker disabled) |
|
||||||
|
| 114 | Dipswitch Front/Rear Mount | 0 | 0 | 0 | |
|
||||||
|
| 115 | Mount Offset Angle | +0 | +0 | +0 | |
|
||||||
|
| 118 | Signal Use LNB Clamp | FALSE | FALSE | FALSE | |
|
||||||
|
|
||||||
|
### PID Gains
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 128 | AZ PID Kp | +600 | +600 | +600 | Proportional |
|
||||||
|
| 129 | AZ PID Kv | +60 | +60 | +60 | Velocity (derivative) |
|
||||||
|
| 130 | AZ PID Ki | +1 | +1 | +1 | Integral |
|
||||||
|
| 131 | EL PID Kp | +250 | +250 | +250 | Proportional |
|
||||||
|
| 132 | EL PID Kv | +50 | +50 | +50 | Velocity (derivative) |
|
||||||
|
| 133 | EL PID Ki | +1 | +1 | +1 | Integral |
|
||||||
|
|
||||||
|
### Motor Stall PWM
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 136 | AZ PWM Stall Cnt | 6 | 6 | 6 | |
|
||||||
|
| 137 | EL PWM Stall Cnt | 5 | 5 | 5 | |
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
| Index | Name | Current | Saved | Default | Notes |
|
||||||
|
|------:|------|---------|-------|---------|-------|
|
||||||
|
| 143 | Tracking Number | 0 | 0 | 0 | |
|
||||||
|
|
||||||
|
## NVS Schema Version
|
||||||
|
|
||||||
|
The NVS schema version is 1.02.13 (reported by `os` -> `id`). This version number is separate from the firmware version (02.02.48) and tracks the storage format itself.
|
||||||
141
src/content/docs/reference/serial-protocol.mdx
Normal file
141
src/content/docs/reference/serial-protocol.mdx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
title: "Serial Protocol"
|
||||||
|
description: RS-485 and RS-422 serial communication protocols for Winegard dishes — bus differences, hardware notes, failsafe concerns, and known hazards.
|
||||||
|
sidebar:
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Winegard dish variants use two different differential signaling standards. Understanding which one your variant uses is essential for choosing the right USB adapter.
|
||||||
|
|
||||||
|
## RS-485 vs RS-422
|
||||||
|
|
||||||
|
### RS-485 Half-Duplex (2-wire)
|
||||||
|
|
||||||
|
One shared differential pair carries both TX and RX. Only one device talks at a time -- the transmitter drives the bus, then releases it so the other side can respond. This is how the Trav'ler IDU communicates with the ODU: the IDU sends a command on the shared T/R pair, then listens for the response on the same wires. Simple wiring (2 signal wires + ground), but throughput is limited by the turn-around time between send and receive.
|
||||||
|
|
||||||
|
### RS-422 Full-Duplex (4-wire)
|
||||||
|
|
||||||
|
Two separate differential pairs -- one dedicated TX pair and one dedicated RX pair. Both sides can transmit simultaneously because the signals do not share wires. Higher throughput (no bus turnaround penalty), and the Carryout G2 uses this at 115200 baud. Point-to-point only (one transmitter per pair).
|
||||||
|
|
||||||
|
### RS-485 Full-Duplex (4-wire)
|
||||||
|
|
||||||
|
Electrically identical to RS-422 wiring (same 4-wire differential pairs), but the RS-485 spec allows multiple transmitters on each pair (multi-drop bus). For our point-to-point dish-to-computer connection, 4-wire RS-485 and RS-422 are interchangeable.
|
||||||
|
|
||||||
|
### Comparison Table
|
||||||
|
|
||||||
|
| Property | RS-485 half-duplex | RS-422 / RS-485 full-duplex |
|
||||||
|
|----------|-------------------|----------------------------|
|
||||||
|
| Signal wires | 2 (+ GND) | 4 (+ GND) |
|
||||||
|
| Direction | One direction at a time | Both directions simultaneously |
|
||||||
|
| Max nodes | 32 drivers + 32 receivers | 1 driver + 10 receivers (RS-422) |
|
||||||
|
| Max distance | 1200m / 4000ft | 1200m / 4000ft |
|
||||||
|
| Max baud | ~10 Mbps | ~10 Mbps |
|
||||||
|
| Voltage swing | +/-1.5V to +/-5V differential | +/-2V to +/-5V differential |
|
||||||
|
| Bus turnaround | Required (adds latency) | Not needed |
|
||||||
|
| Typical adapter | USB-to-RS485 (DTECH, etc.) | USB-to-RS422 (FTDI, DIYables, etc.) |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The Trav'ler's RJ-25 connector exposes **both** a half-duplex pair (pins 2-3, labeled T/R) **and** a dedicated receive pair (pins 4-5, labeled RXD). Gabe's code uses only the half-duplex pair via an RS-485 adapter. Davidson's G2 code uses all four wires as RS-422. The same physical connector may support both modes depending on the firmware -- this is unconfirmed on the Trav'ler but worth testing if you have a 4-wire adapter available.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Hardware Protocol Notes
|
||||||
|
|
||||||
|
- RS-485 serial, 57600 baud, 8N1 (Trav'ler variants) or RS-422, 115200 baud, 8N1 (Carryout G2)
|
||||||
|
- Motor commands: `a <motor_id> <degrees>` (0=azimuth, 1=elevation)
|
||||||
|
- Position query: `a` (in motor submenu) returns position in variant-specific format
|
||||||
|
- Two motor control methods:
|
||||||
|
- **`a <id> <deg>`** -- queues command, waits for current motor to finish. Tolerant of rapid command streams from Gpredict.
|
||||||
|
- **`g <az> <el> <sk>`** -- immediate move, aborts on any new keystroke/character. Some firmware has a typo listing the order as az/sk/el.
|
||||||
|
- Skew motor can run simultaneously with AZ or EL (the others are mutually exclusive)
|
||||||
|
- Elevation floor: HAL 2.05 unreliable below 15 degrees with direct motor commands
|
||||||
|
- Cable wrap limit: usually 360 or 455 degrees, dish reverses at limit
|
||||||
|
- Console does not accept backspace -- hit enter to clear on typo
|
||||||
|
- Firmware prompt character is `>` (ASCII 62) -- used for reliable response termination in prompt-terminated read strategies
|
||||||
|
- Firmware expects ASCII CR (`0x0D`) as line terminator
|
||||||
|
|
||||||
|
## RS-422 Module Notes (DIYables MAX490)
|
||||||
|
|
||||||
|
The DIYables RS422-to-TTL module uses the **MAX490** transceiver chip (2.5 Mbps max, well above the 115200 baud requirement).
|
||||||
|
|
||||||
|
### Module Specs
|
||||||
|
|
||||||
|
- 5V TTL logic on the microcontroller side (RXD/TXD)
|
||||||
|
- 15 kV ESD protection on RS-422 lines
|
||||||
|
- TVS diode for lightning/spike suppression
|
||||||
|
- 10 ohm current-limiting resistors for overcurrent protection
|
||||||
|
- Built-in 120 ohm termination resistor (reduces echo on long runs)
|
||||||
|
- Power + TX/RX activity LEDs
|
||||||
|
- Board size: 5.0cm x 2.7cm
|
||||||
|
|
||||||
|
### Failsafe Concern
|
||||||
|
|
||||||
|
<Aside type="caution" title="Bus float on idle">
|
||||||
|
The MAX490 does not have failsafe logic, and the module has no provisions for passive failsafe bias resistors. When the RS-422 bus tri-states (no driver active -- e.g., between commands, during power transitions, or if the dish firmware is slow to respond), the receiver inputs float and may see random transitions interpreted as garbage data. This can cause spurious bytes in the serial stream.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
**Workaround options:**
|
||||||
|
|
||||||
|
1. **Add external bias resistors** -- pull A/RX+ toward V+ and B/RX- toward GND through ~560 ohm resistors. This biases the idle bus to a known logic-high state (RS-422 "mark" / idle). Solder to the module or add inline on the RJ-12 breakout.
|
||||||
|
|
||||||
|
2. **Use the prompt-terminated read strategy** -- the `CarryoutG2Protocol._send()` reads until `>` (ASCII 62) which naturally filters out garbage between commands, since random transitions are unlikely to produce a valid `>` in context.
|
||||||
|
|
||||||
|
3. **Ignore idle noise in firmware** -- the Winegard firmware likely ignores unexpected input while it is processing or idle, but any bytes received during the bus float could corrupt the next valid command if they land in the UART buffer at the wrong time.
|
||||||
|
|
||||||
|
For short cable runs (under ~3m between ESP32 and dish), the built-in 120 ohm termination is sufficient and bus float is less likely to cause issues. For longer runs or electrically noisy environments (near motors, power supplies), add the bias resistors.
|
||||||
|
|
||||||
|
## Known Console Hazards
|
||||||
|
|
||||||
|
<Aside type="danger" title="Shell deadlock and termination">
|
||||||
|
**ADC `scan` without arguments on uncalibrated AZ:** Targets position 2147483647 (INT_MAX), motor task blocks forever, shell deadlocks. No serial input (CR, Ctrl+C, ESC, `q`, `reboot`) can recover -- requires hardware power cycle. The firmware shell is single-threaded: UART input is only parsed between command completions, so a blocking motor move prevents all input.
|
||||||
|
|
||||||
|
**Root `q` command:** Terminates the shell task entirely. Console becomes unresponsive until power cycle (same as deadlock, but intentional).
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Command Response Format
|
||||||
|
|
||||||
|
### One-Shot Commands
|
||||||
|
|
||||||
|
Execute once, return a result, and show the prompt (`>`). Safe to use programmatically.
|
||||||
|
|
||||||
|
```
|
||||||
|
a
|
||||||
|
Angle[0] = 180.00
|
||||||
|
Angle[1] = 45.00
|
||||||
|
MOT>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming Commands
|
||||||
|
|
||||||
|
Run indefinitely until interrupted. Some accept `q` to stop, others require closing the serial port. Known streamers include `ts`, `agc`, `lnbv`, `snr`, `qls`, `nid`, and `stats`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Reads:1 SNR[avg: 0.0 cur: 0.0]
|
||||||
|
Reads:2 SNR[avg: 0.0 cur: 0.0]
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Write Commands
|
||||||
|
|
||||||
|
Modify state (motor position, NVS values, fault registers). The firmware provides no confirmation prompts for destructive operations.
|
||||||
|
|
||||||
|
## Prompt-Terminated Reading
|
||||||
|
|
||||||
|
The most reliable strategy for programmatic serial communication is to read until the prompt character `>` (ASCII 62) appears. This is what the `CarryoutG2Protocol._send()` method does:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _send(self, cmd: str) -> str:
|
||||||
|
self._serial.write(f"{cmd}\r".encode("ascii"))
|
||||||
|
resp_data = bytearray()
|
||||||
|
while True:
|
||||||
|
byte = self._serial.read(1)
|
||||||
|
if len(byte) == 0:
|
||||||
|
raise TimeoutError(f"No prompt after command: {cmd!r}")
|
||||||
|
resp_data.append(byte[0])
|
||||||
|
if byte[0] == 62: # ASCII '>'
|
||||||
|
break
|
||||||
|
return resp_data.decode("utf-8", errors="ignore")
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach handles variable-length responses and is resilient to bus noise, since random garbage is unlikely to produce a valid `>` in the expected context.
|
||||||
245
src/content/docs/understanding/architecture.mdx
Normal file
245
src/content/docs/understanding/architecture.mdx
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
---
|
||||||
|
title: Software Architecture
|
||||||
|
description: How the birdcage and console-probe packages are structured, and why each layer exists.
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The project contains two Python packages. `birdcage` controls the dish for satellite tracking. `console-probe` is a separate tool for exploring and mapping unknown firmware consoles. They share no code at runtime but were developed together -- console-probe is how we learned enough about the firmware to write the protocol implementations in birdcage.
|
||||||
|
|
||||||
|
## birdcage
|
||||||
|
|
||||||
|
The control stack is five layers deep, each with a single responsibility:
|
||||||
|
|
||||||
|
```
|
||||||
|
cli.py Click CLI: init / serve / pos / move
|
||||||
|
|
|
||||||
|
rotctld.py Hamlib rotctld TCP server (p/P/S/_/q + R/L/D extensions)
|
||||||
|
|
|
||||||
|
antenna.py BirdcageAntenna: high-level control, motor alternation, elevation floor
|
||||||
|
|
|
||||||
|
leapfrog.py Pure function: predictive overshoot compensation
|
||||||
|
|
|
||||||
|
protocol.py FirmwareProtocol ABC + HAL205 / HAL000 / CarryoutG2 subclasses
|
||||||
|
(owns the serial port)
|
||||||
|
```
|
||||||
|
|
||||||
|
Data flows top-down: Gpredict sends a position command to the rotctld server, which calls the antenna, which applies leapfrog correction and hands the adjusted target to the protocol, which sends the serial bytes to the dish.
|
||||||
|
|
||||||
|
### protocol.py -- firmware abstraction
|
||||||
|
|
||||||
|
This is where the serial port lives. The `FirmwareProtocol` abstract base class defines the contract that every firmware variant must implement:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class FirmwareProtocol(ABC):
|
||||||
|
def connect(self, port: str, baudrate: int = 57600) -> None: ...
|
||||||
|
def disconnect(self) -> None: ...
|
||||||
|
def get_position(self) -> Position: ...
|
||||||
|
def move_motor(self, motor_id: int, degrees: float) -> None: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def initialize(self, callback: Callable[[str], None] | None = None) -> None: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def enter_motor_menu(self) -> None: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def kill_search(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Three concrete subclasses implement this interface:
|
||||||
|
|
||||||
|
**`HAL205Protocol`** -- for HAL 2.05.003 firmware. Boot signals are `NoGPS` or `No LNB Voltage`. Enters the motor menu via the `motor` command. Kills the search by navigating `ngsearch` -> `s` -> `q`.
|
||||||
|
|
||||||
|
**`HAL000Protocol`** -- for HAL 0.0.00 firmware (older Trav'ler units). Same boot signal (`NoGPS`), but the motor command is `mot` and search kill goes through the OS task manager: `os` -> `kill Search` -> `q`.
|
||||||
|
|
||||||
|
**`CarryoutG2Protocol`** -- for the Carryout G2. RS-422 at 115200 baud instead of RS-485 at 57600. Search is disabled permanently via NVS, so `kill_search()` is a no-op. This subclass also adds methods not in the ABC: `home_motor()`, `enter_dvb_menu()`, `enable_lna()`, and `get_rssi()`.
|
||||||
|
|
||||||
|
The key architectural decision: **the protocol owns all serial I/O**. Nothing above this layer touches `pyserial` directly. The base class provides `_write()` and `_read()` for simple command/response. The G2 subclass overrides this with `_send()`, which reads byte-by-byte until the `>` prompt character (ASCII 62) -- more reliable than fixed-buffer reads because the firmware always emits `>` when ready.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _send(self, cmd: str) -> str:
|
||||||
|
"""Send a command and read until the '>' prompt character."""
|
||||||
|
self._serial.write(f"{cmd}\r".encode("ascii"))
|
||||||
|
|
||||||
|
resp_data: bytearray = bytearray()
|
||||||
|
while True:
|
||||||
|
byte = self._serial.read(1)
|
||||||
|
if len(byte) == 0:
|
||||||
|
raise TimeoutError(f"No prompt after command: {cmd!r}")
|
||||||
|
resp_data.append(byte[0])
|
||||||
|
if byte[0] == self.PROMPT_CHAR:
|
||||||
|
break
|
||||||
|
|
||||||
|
return resp_data.decode("utf-8", errors="ignore")
|
||||||
|
```
|
||||||
|
|
||||||
|
A firmware registry maps short names to classes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
FIRMWARE_REGISTRY: dict[str, type[FirmwareProtocol]] = {
|
||||||
|
"hal205": HAL205Protocol,
|
||||||
|
"hal000": HAL000Protocol,
|
||||||
|
"g2": CarryoutG2Protocol,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI uses `get_protocol("g2")` to instantiate the right class. Adding a new firmware variant means writing a new subclass and adding it to this dictionary.
|
||||||
|
|
||||||
|
### leapfrog.py -- mechanical lag compensation
|
||||||
|
|
||||||
|
A single pure function with no side effects:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def apply_leapfrog(
|
||||||
|
target_az: float, target_el: float,
|
||||||
|
current_az: float, current_el: float,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
```
|
||||||
|
|
||||||
|
For each axis, if the delta between target and current position exceeds a threshold, the target is nudged further in the direction of travel:
|
||||||
|
|
||||||
|
| Delta | Overshoot |
|
||||||
|
|-------|-----------|
|
||||||
|
| More than 2 degrees | +/- 1.0 degree |
|
||||||
|
| More than 1 degree | +/- 0.5 degree |
|
||||||
|
| 1 degree or less | No adjustment |
|
||||||
|
|
||||||
|
This compensates for the time it takes stepper motors to physically reach a position -- by the time the dish arrives, the satellite has moved further along its track.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The original upstream code had a copy-paste bug where the elevation section modified `target_az` instead of `target_el`. This meant elevation never got leap-frog correction, and azimuth got double correction. See [Known Bugs](/understanding/known-bugs/) for the full analysis.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### antenna.py -- the consumer-facing API
|
||||||
|
|
||||||
|
`BirdcageAntenna` is what everything above the protocol should call. It wraps three concerns:
|
||||||
|
|
||||||
|
1. **Lifecycle management** -- connect, initialize (boot wait + search kill + motor menu entry), disconnect
|
||||||
|
2. **Leap-frog integration** -- applies `apply_leapfrog()` to every move if `config.leapfrog_enabled` is true
|
||||||
|
3. **Motor command alternation** -- even-numbered moves send AZ first then EL; odd moves reverse the order. This prevents one axis from starving the other on the shared serial bus.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BirdcageAntenna:
|
||||||
|
def __init__(self, protocol: FirmwareProtocol, config: AntennaConfig | None = None):
|
||||||
|
...
|
||||||
|
|
||||||
|
def initialize(self) -> None: ...
|
||||||
|
def get_position(self) -> Position: ...
|
||||||
|
def move_to(self, azimuth: float, elevation: float) -> None: ...
|
||||||
|
def stop(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`AntennaConfig` holds serial port, baud rate, minimum elevation, and the leapfrog toggle. The G2 defaults to 115200 baud and 18-degree minimum elevation; the Trav'ler defaults to 57600 and 15 degrees.
|
||||||
|
|
||||||
|
### rotctld.py -- Gpredict bridge
|
||||||
|
|
||||||
|
A plain TCP socket server implementing the subset of the Hamlib rotctld protocol that Gpredict uses:
|
||||||
|
|
||||||
|
| Command | Handler | Response |
|
||||||
|
|---------|---------|----------|
|
||||||
|
| `p` | `_handle_get_position()` | `<az>\n<el>\n` |
|
||||||
|
| `P <az> <el>` | `_handle_set_position()` | `RPRT 0\n` or `RPRT -1\n` |
|
||||||
|
| `S` | `_handle_stop()` | (closes connection) |
|
||||||
|
| `_` | `_handle_model_name()` | `Winegard Trav'ler RS-485 Rotor\n` |
|
||||||
|
| `q` | (break loop) | (closes connection) |
|
||||||
|
|
||||||
|
For CarryoutG2 specifically, three extension commands are available:
|
||||||
|
|
||||||
|
| Command | Handler | Function |
|
||||||
|
|---------|---------|----------|
|
||||||
|
| `R [n]` | `_handle_read_rssi()` | Read RSSI averaged over n samples |
|
||||||
|
| `L` | `_handle_enable_lna()` | Enable LNA for signal reception |
|
||||||
|
| `D` | `_handle_capabilities()` | Report supported extensions |
|
||||||
|
|
||||||
|
The RSSI handler is noteworthy -- it has to switch firmware submenus mid-operation. It exits the motor menu, enters the DVB menu, reads RSSI, exits DVB, and re-enters the motor menu. Non-G2 rotors return `RPRT -6` (not available) for these commands.
|
||||||
|
|
||||||
|
### cli.py -- the entry point
|
||||||
|
|
||||||
|
A Click CLI with four subcommands:
|
||||||
|
|
||||||
|
- **`init`** -- connect, wait for boot, kill search, enter motor menu
|
||||||
|
- **`serve`** -- run the rotctld TCP server (optionally skipping init)
|
||||||
|
- **`pos`** -- query and print current AZ/EL
|
||||||
|
- **`move`** -- send a single move command to a specific AZ/EL
|
||||||
|
|
||||||
|
All subcommands accept `--port` and `--firmware` options, with environment variable fallbacks (`BIRDCAGE_PORT`, `BIRDCAGE_FIRMWARE`).
|
||||||
|
|
||||||
|
## console-probe
|
||||||
|
|
||||||
|
The probe tool has a parallel but simpler architecture:
|
||||||
|
|
||||||
|
```
|
||||||
|
cli.py argparse CLI: --discover-only, --deep, --submenu, --json
|
||||||
|
|
|
||||||
|
report.py JSON report generation (format_version 2)
|
||||||
|
|
|
||||||
|
discovery.py Auto-discovery, help parsing, submenu probing, candidate generation
|
||||||
|
|
|
||||||
|
serial_io.py Prompt-aware serial I/O
|
||||||
|
|
|
||||||
|
profile.py DeviceProfile + HelpEntry dataclasses
|
||||||
|
```
|
||||||
|
|
||||||
|
### profile.py -- device model
|
||||||
|
|
||||||
|
Everything known about the attached console is stored in a `DeviceProfile`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class DeviceProfile:
|
||||||
|
port: str = "/dev/ttyUSB0"
|
||||||
|
baud: int = 115200
|
||||||
|
root_prompt: str = "" # e.g. "TRK>"
|
||||||
|
prompts: list[str] = ... # all known prompts
|
||||||
|
error_string: str = "" # e.g. "Invalid command."
|
||||||
|
known_commands: set[str] = ... # from help output
|
||||||
|
submenus: list[str] = ... # detected submenu names
|
||||||
|
exit_cmd: str = "q"
|
||||||
|
line_ending: str = "\r"
|
||||||
|
submenu_help: dict[str, list[HelpEntry]] = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Commands parsed from help output are captured as `HelpEntry` objects with name, description, and parameter syntax.
|
||||||
|
|
||||||
|
### serial_io.py -- prompt-terminated reads
|
||||||
|
|
||||||
|
The key insight in console-probe's serial I/O is the `_is_prompt_terminated()` function. Instead of reading a fixed number of bytes or waiting for a timeout, it checks whether the response ends with a recognized prompt string.
|
||||||
|
|
||||||
|
This required solving a subtle bug: help text like `help [<command>]` contains the `>` character inside parameter syntax (`<command>`). The function distinguishes between actual prompts and parameter syntax by checking for `[` brackets on the last line:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _is_prompt_terminated(text: str, profile: DeviceProfile) -> bool:
|
||||||
|
last_line = stripped.split("\n")[-1]
|
||||||
|
|
||||||
|
if profile.prompts:
|
||||||
|
# Check known prompts (fast path)
|
||||||
|
for p in profile.prompts:
|
||||||
|
if last_stripped.endswith(p):
|
||||||
|
return True
|
||||||
|
# Accept PROMPT_RE match only if no brackets on that line
|
||||||
|
if "[" not in last_line:
|
||||||
|
m = PROMPT_RE.search(last_line)
|
||||||
|
if m:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# No known prompts yet -- fallback to bare > check
|
||||||
|
return stripped.endswith(">")
|
||||||
|
```
|
||||||
|
|
||||||
|
### discovery.py -- the exploration engine
|
||||||
|
|
||||||
|
This module handles several distinct tasks:
|
||||||
|
|
||||||
|
**Help parsing** -- `parse_help_output()` extracts command names and submenu hints from firmware help text. It handles multiple formats: `command - description`, angle-bracket syntax (`Enter <a3981>`), double-space separated columns, and bare command names. A set of known parameter placeholders (`command`, `value`, `index`, etc.) prevents false positives.
|
||||||
|
|
||||||
|
**Submenu probing** -- `discover_submenu_help()` queries help in the current submenu and tries multiple help commands (`?` and `man`) for firmware with paginated output.
|
||||||
|
|
||||||
|
**Error detection** -- `detect_error_string()` sends a garbage command and captures the firmware's error message, which is then used to distinguish real command responses from error responses during probing.
|
||||||
|
|
||||||
|
**Command probing** -- `probe_commands()` iterates through candidate command strings, sends each one, and checks whether the response differs from the error string. It recovers from accidental submenu exits and shell terminations.
|
||||||
|
|
||||||
|
**Candidate generation** -- `generate_candidates()` builds a list of potential commands from single characters, two-letter combinations, common embedded debug commands (memory access, flash, boot, GPIO, SPI, I2C, etc.), and optional external wordlists.
|
||||||
204
src/content/docs/understanding/hardware-platform.mdx
Normal file
204
src/content/docs/understanding/hardware-platform.mdx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
---
|
||||||
|
title: Hardware Platform
|
||||||
|
description: Deep dive into the Carryout G2's internal hardware — MCU, motor drivers, DVB tuner, and SPI bus topology.
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Carryout G2 is the most thoroughly documented variant in this project. Its firmware is verbose at boot and exposes low-level hardware state through GPIO, ADC, and SPI submenu commands. This page covers what we've identified about the three major silicon components and how they interconnect.
|
||||||
|
|
||||||
|
## System overview
|
||||||
|
|
||||||
|
The G2 runs on three chips connected by two SPI buses:
|
||||||
|
|
||||||
|
```
|
||||||
|
SPI1 (4 MHz)
|
||||||
|
K60 MCU ────────────────────────── 2x A3981 (Stepper Drivers)
|
||||||
|
(Cortex-M4) AZ motor (40000 steps/rev)
|
||||||
|
| EL motor (24960 steps/rev)
|
||||||
|
|
|
||||||
|
| SPI2 (6.857 MHz)
|
||||||
|
└──────────────────────────── BCM4515 (DVB-S2 Tuner)
|
||||||
|
RSSI, SNR, lock detect
|
||||||
|
DiSEqC 2.x controller
|
||||||
|
```
|
||||||
|
|
||||||
|
The MCU runs the firmware shell, motor control loop, and DVB signal processing. The motor drivers are pure slave peripherals -- they receive step/direction commands over SPI and handle microstepping and current regulation. The DVB tuner is also a slave but much more complex, running its own firmware for RF signal processing.
|
||||||
|
|
||||||
|
## NXP MK60DN512VLQ10 (K60 MCU)
|
||||||
|
|
||||||
|
| Spec | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Core | ARM Cortex-M4 |
|
||||||
|
| Clock | 96 MHz |
|
||||||
|
| Flash | 512 KB |
|
||||||
|
| RAM | 128 KB SRAM |
|
||||||
|
| Package | 144-LQFP |
|
||||||
|
| FlexNVM | EEPROM emulation (EE> submenu) |
|
||||||
|
| GPIO | 5 ports (A-E), ~92 pins bonded |
|
||||||
|
| SPI | 3 controllers (SPI0-SPI2) |
|
||||||
|
| UART | 6 controllers (UART0-UART5) |
|
||||||
|
|
||||||
|
The K60 is an NXP Kinetis part, part of the K60 sub-family designed for industrial and motor control applications. Its Cortex-M4 core includes a single-precision FPU, which the firmware uses for angle calculations (position readouts are floating-point).
|
||||||
|
|
||||||
|
<Aside type="note" title="Firmware version">
|
||||||
|
The boot log reports firmware version **02.02.48**, copyright 2013, Winegard Company. Bootloader version 1.01. The firmware is stored in the K60's internal 512 KB flash and runs directly from there (no external code memory).
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### UART4 -- serial console
|
||||||
|
|
||||||
|
The firmware console runs on UART4 at 115200 baud, 8N1:
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | Function |
|
||||||
|
|---------|------|----------|
|
||||||
|
| PTE24 | E24 | UART4_TX (to computer RX pair) |
|
||||||
|
| PTE25 | E25 | UART4_RX (from computer TX pair) |
|
||||||
|
| PTE26 | E26 | UART4_CTS (hardware flow control, idle high) |
|
||||||
|
|
||||||
|
CTS is connected but the firmware doesn't appear to require hardware flow control -- the console works fine without it.
|
||||||
|
|
||||||
|
### GPIO mapping
|
||||||
|
|
||||||
|
Live GPIO probing (`gpio regs`, `gpio dir`, `gpio r`) across ports A-E revealed 92 pins. Notable patterns:
|
||||||
|
|
||||||
|
- **Port E** is densely used: SPI1 (E0-E5), UART4 (E24-E27), plus unidentified pins
|
||||||
|
- **Port D** carries SPI2 (D11-D15) and an output on D10 (likely BCM4515 reset/enable)
|
||||||
|
- **Port B** has a cluster at B0-B3 (possibly SPI0 or I2C) and B11 (status LED or peripheral enable)
|
||||||
|
- **Port C** has a contiguous block at C10-C13 (bus interface) and C18 (LNB voltage control)
|
||||||
|
- **Absent pins:** A20-A23 and B12-B15 are not bonded on this package variant
|
||||||
|
|
||||||
|
## Allegro A3981 (Stepper Motor Drivers)
|
||||||
|
|
||||||
|
Two A3981 ICs, one per motor axis, controlled over SPI1.
|
||||||
|
|
||||||
|
| Spec | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Interface | SPI (mode 0x03) at 4 MHz |
|
||||||
|
| Microstepping | Up to 1/16 step, AUTO mode |
|
||||||
|
| Current control | AUTO mode (adapts to load) |
|
||||||
|
| Fault detection | DIAG pin per driver (active-low open-drain) |
|
||||||
|
| Step resolution | Full, Half, Quarter, Eighth, Sixteenth |
|
||||||
|
|
||||||
|
### Motor specifications
|
||||||
|
|
||||||
|
| Parameter | AZ (Motor 0) | EL (Motor 1) |
|
||||||
|
|-----------|--------------|--------------|
|
||||||
|
| Steps per revolution | 40000 | 24960 |
|
||||||
|
| Max velocity | 65.00 deg/s | 45.00 deg/s |
|
||||||
|
| Max acceleration | 400.00 deg/s^2 | (from NVS) |
|
||||||
|
| Step velocity (raw) | 7222 ustep/s | 3120 ustep/s |
|
||||||
|
| Step acceleration (raw) | 44 ustep/s/ms | 28 ustep/s/ms |
|
||||||
|
| Gear ratio | 1.602564 | (from boot log) |
|
||||||
|
|
||||||
|
The AZ motor is the "master" (more steps, faster) and drives the entire dish rotation. The EL motor is the "slave" with fewer steps per revolution.
|
||||||
|
|
||||||
|
### SPI1 pin assignments
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | SPI Function | Notes |
|
||||||
|
|---------|------|-------------|-------|
|
||||||
|
| PTE1 | E1 | SPI1_SOUT | MOSI -- MCU to A3981 |
|
||||||
|
| PTE2 | E2 | SPI1_SCK | SPI clock |
|
||||||
|
| PTE3 | E3 | SPI1_SIN | MISO -- A3981 to MCU |
|
||||||
|
| PTE4 | E4 | SPI1_PCS0 | Chip select: AZ motor driver |
|
||||||
|
| PTE0 | E0 | SPI1_PCS1 | Chip select: EL motor driver |
|
||||||
|
| PTE5 | E5 | SPI1_PCS2 | Possibly A3981 RESET or enable |
|
||||||
|
|
||||||
|
<Aside type="note" title="AUTO mode">
|
||||||
|
The `a3981 cm` command reports both drivers in "AUTO" current mode, and `a3981 sm` reports "AUTO" step mode. In AUTO mode, the A3981 selects the microstepping level based on the commanded step rate -- coarser steps at high speed for efficiency, finer steps at low speed for precision.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Motor control hierarchy
|
||||||
|
|
||||||
|
Three layers of motor control exist in the firmware:
|
||||||
|
|
||||||
|
1. **STEP submenu** -- raw stepper API in microstep units. Commands like `p [motor] [steps]` and `v [motor] [ustep/sec]` operate directly on the step counters.
|
||||||
|
2. **MOT submenu** -- angle-based API. The `a <id> <deg>` command converts degrees to steps using the steps-per-revolution values and manages PID control.
|
||||||
|
3. **BirdcageAntenna** (our code) -- adds leap-frog compensation and motor alternation on top of MOT commands.
|
||||||
|
|
||||||
|
### Homing and calibration
|
||||||
|
|
||||||
|
On boot (when tracker is enabled), the firmware runs a homing sequence:
|
||||||
|
|
||||||
|
```
|
||||||
|
EL home: stall detect, 2 second timeout
|
||||||
|
AZ home: stall detect, 8 second timeout
|
||||||
|
"Antenna Facing Front"
|
||||||
|
```
|
||||||
|
|
||||||
|
The dish uses motor stalling (not limit switches) to find mechanical boundaries. The A3981's stall detection watches for back-EMF signatures that indicate the motor has hit a hard stop.
|
||||||
|
|
||||||
|
After homing, the firmware reports cable wrap limits: `wrap_min:-42333 wrap_max:2333` (centidegrees). Total AZ range is approximately 446.66 degrees.
|
||||||
|
|
||||||
|
The `h <id>` command triggers explicit homing for a single motor outside the boot sequence.
|
||||||
|
|
||||||
|
<Aside type="caution" title="Unhomed axes">
|
||||||
|
When NVS 20 is TRUE (tracker disabled), homing is skipped entirely on boot. Motor positions read as INT_MAX (2147483647) until manually homed with `h 0` and `h 1`. The ADC `scan` command will target INT_MAX on an unhomed axis and deadlock the shell.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Broadcom BCM4515 (DVB-S2 Tuner)
|
||||||
|
|
||||||
|
| Spec | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Chip ID | 0x4515 |
|
||||||
|
| Silicon revision | B0 |
|
||||||
|
| Firmware version | v113.37 |
|
||||||
|
| Strap config | 0x25018 |
|
||||||
|
| Interface | SPI (mode 0x03) at 6.857 MHz |
|
||||||
|
| Standard | DVB-S2 (Digital Video Broadcasting - Satellite, 2nd gen) |
|
||||||
|
| Search range | 18000-24000 ksps, rolloff 0.35 |
|
||||||
|
|
||||||
|
The BCM4515 is a highly integrated DVB-S2 demodulator. It handles the entire receive chain from IF input to transport stream output: RF/IF AGC, carrier recovery, timing recovery, LDPC/BCH decoding, and baseband processing.
|
||||||
|
|
||||||
|
### SPI2 pin assignments
|
||||||
|
|
||||||
|
| K60 Pin | GPIO | SPI Function | Notes |
|
||||||
|
|---------|------|-------------|-------|
|
||||||
|
| PTD12 | D12 | SPI2_SCK | SPI clock |
|
||||||
|
| PTD13 | D13 | SPI2_SOUT | MOSI -- MCU to BCM4515 |
|
||||||
|
| PTD14 | D14 | SPI2_SIN | MISO -- BCM4515 to MCU |
|
||||||
|
| PTD11 | D11 | SPI2_PCS0 | Chip select |
|
||||||
|
| PTD15 | D15 | SPI2_PCS1 | Secondary chip select (unused) |
|
||||||
|
| PTD10 | D10 | GPIO (OUT) | Likely BCM4515 reset or power enable |
|
||||||
|
|
||||||
|
### Signal measurement capabilities
|
||||||
|
|
||||||
|
The DVB submenu exposes several signal measurement modes useful for ham radio:
|
||||||
|
|
||||||
|
**RSSI** -- `rssi <n>` returns the average and current raw ADC values over n samples. Noise floor is approximately 500. Used for RF power measurement at each pointing position.
|
||||||
|
|
||||||
|
**AGC** -- `agc` streams real-time RF/IF AGC levels plus SNR and NID (Network ID). This is a continuous output interrupted by `q`.
|
||||||
|
|
||||||
|
**SNR** -- Signal-to-noise ratio in dB.
|
||||||
|
|
||||||
|
**Lock status** -- `ls` returns total reads, no-signal count, glitch count, and NID table. `qls` gives a quick check.
|
||||||
|
|
||||||
|
**LNB control** -- `lnbdc odu` sets the LNB to 13V (V-polarization). Boot default is 18V (H-polarization). The `lnbv` command streams the actual voltage being applied.
|
||||||
|
|
||||||
|
### DiSEqC 2.x interface
|
||||||
|
|
||||||
|
The BCM4515 includes a DiSEqC controller for LNB switch control. DiSEqC uses 22 kHz tone bursts superimposed on the coax LNB bias line. The DVB submenu provides:
|
||||||
|
|
||||||
|
- **`di2conf`** -- LNB config register read
|
||||||
|
- **`di2id`** -- Hardware ID query
|
||||||
|
- **`di2stat`** -- LNB status flags
|
||||||
|
- **`send <hex>`** -- Raw DiSEqC packet transmission (up to 6 bytes)
|
||||||
|
- **`ovraddr`** -- Override target address (default 0x11)
|
||||||
|
|
||||||
|
Without an external DiSEqC switch connected, these commands return `RxReplyTimeout`.
|
||||||
|
|
||||||
|
### Radio telescope mode
|
||||||
|
|
||||||
|
The `azscanwxp` command in the MOT submenu performs an azimuth sweep while cycling through DVB transponders at each position. At each step, it records motor angle, RSSI, lock status, SNR, and scan delta. Combined with elevation stepping, this produces a 2D RF power map of the sky -- essentially using the dish as a radio telescope.
|
||||||
|
|
||||||
|
```
|
||||||
|
azscanwxp [motor] [span_deg] [resolution_cdeg] [num_xponders]
|
||||||
|
```
|
||||||
|
|
||||||
|
Output per position:
|
||||||
|
```
|
||||||
|
Motor:<id> Angle:<cdeg> RSSI:<adc> Lock:<0/1> SNR:<dB> Scan Delta:<step>
|
||||||
|
```
|
||||||
|
|
||||||
|
This capability exists in the stock firmware -- it was designed for Winegard's factory alignment and satellite acquisition testing, but it works just as well for amateur radio sky mapping.
|
||||||
192
src/content/docs/understanding/known-bugs.mdx
Normal file
192
src/content/docs/understanding/known-bugs.mdx
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
title: Known Bugs
|
||||||
|
description: Documented bugs in the upstream code and firmware, with analysis and fixes.
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This page documents bugs we've found and fixed in the upstream code, as well as firmware hazards that can't be fixed in software but need to be understood.
|
||||||
|
|
||||||
|
## Leap-frog elevation bug (upstream)
|
||||||
|
|
||||||
|
**Location:** `Trav-ler-Rotor-For-HAL-2.05/travler_rotor.py`, lines 98-105
|
||||||
|
|
||||||
|
**Severity:** Tracking accuracy degraded on the elevation axis
|
||||||
|
|
||||||
|
**Affected repos:**
|
||||||
|
- `saveitforparts/Trav-ler-Rotor-For-HAL-2.05` -- `travler_rotor.py` lines 98-105
|
||||||
|
- `saveitforparts/Travler-Pro-Rotor` -- same bug copy-pasted into `travler_pro_rotor.py`
|
||||||
|
|
||||||
|
### The bug
|
||||||
|
|
||||||
|
The leap-frog algorithm has two sections: azimuth compensation and elevation compensation. Both were written from the same template. In the elevation section, a copy-paste error left `target_az` as the variable being modified instead of changing it to `target_el`.
|
||||||
|
|
||||||
|
**Original code (lines 90-105):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Azimuth compensation (correct)
|
||||||
|
if target_az - current_az > 2:
|
||||||
|
target_az+=1
|
||||||
|
elif target_az - current_az < -2:
|
||||||
|
target_az-=1
|
||||||
|
elif target_az - current_az > 1:
|
||||||
|
target_az+=0.5
|
||||||
|
elif target_az - current_az < -1:
|
||||||
|
target_az-=0.5
|
||||||
|
|
||||||
|
# Elevation compensation (BUG: modifies target_az instead of target_el)
|
||||||
|
if target_el - current_el > 2:
|
||||||
|
target_az+=1 # <-- should be target_el
|
||||||
|
elif target_el - current_el < -2:
|
||||||
|
target_az-=1 # <-- should be target_el
|
||||||
|
elif target_el - current_el > 1:
|
||||||
|
target_az+=0.5 # <-- should be target_el
|
||||||
|
elif target_el - current_el < -1:
|
||||||
|
target_az-=0.5 # <-- should be target_el
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
|
||||||
|
Two things go wrong simultaneously:
|
||||||
|
|
||||||
|
1. **Elevation never gets leap-frog compensation.** The elevation delta is computed correctly (`target_el - current_el`), but the adjustment is applied to the wrong variable. During fast satellite passes with significant elevation change, the dish lags behind on the EL axis.
|
||||||
|
|
||||||
|
2. **Azimuth gets double compensation.** The azimuth correction from its own section is applied first, then the elevation section adds a second correction to the same variable. If both axes have large deltas (common during a pass), azimuth overshoots its target.
|
||||||
|
|
||||||
|
For a satellite pass where both AZ and EL are changing by more than 2 degrees per update:
|
||||||
|
- AZ gets +2.0 degrees of correction (1.0 from its own section + 1.0 from the elevation section)
|
||||||
|
- EL gets +0.0 degrees of correction
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
|
||||||
|
In `birdcage/leapfrog.py`, the elevation section correctly modifies `target_el`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def apply_leapfrog(
|
||||||
|
target_az: float,
|
||||||
|
target_el: float,
|
||||||
|
current_az: float,
|
||||||
|
current_el: float,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
# Azimuth compensation
|
||||||
|
az_delta = target_az - current_az
|
||||||
|
if abs(az_delta) > 2:
|
||||||
|
target_az += 1.0 if az_delta > 0 else -1.0
|
||||||
|
elif abs(az_delta) > 1:
|
||||||
|
target_az += 0.5 if az_delta > 0 else -0.5
|
||||||
|
|
||||||
|
# Elevation compensation (fixed: modifies target_el, not target_az)
|
||||||
|
el_delta = target_el - current_el
|
||||||
|
if abs(el_delta) > 2:
|
||||||
|
target_el += 1.0 if el_delta > 0 else -1.0
|
||||||
|
elif abs(el_delta) > 1:
|
||||||
|
target_el += 0.5 if el_delta > 0 else -0.5
|
||||||
|
|
||||||
|
return target_az, target_el
|
||||||
|
```
|
||||||
|
|
||||||
|
The fix also restructures the conditionals to use `abs()` and ternary expressions, making the symmetry between the two axes explicit and harder to get wrong in future edits.
|
||||||
|
|
||||||
|
## Prompt termination bug (console-probe)
|
||||||
|
|
||||||
|
**Location:** `console_probe/serial_io.py`, `_is_prompt_terminated()`
|
||||||
|
|
||||||
|
**Severity:** False prompt detection causes truncated responses
|
||||||
|
|
||||||
|
### The bug
|
||||||
|
|
||||||
|
The original prompt detection logic checked whether the response ended with `>`. This worked for the firmware prompt (`TRK>`, `MOT>`, etc.) but also matched the `>` character inside parameter syntax in help text:
|
||||||
|
|
||||||
|
```
|
||||||
|
help [<command>]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `>` in `<command>` would trigger prompt detection, cutting off the rest of the help output.
|
||||||
|
|
||||||
|
### The fix
|
||||||
|
|
||||||
|
The `_is_prompt_terminated()` function now distinguishes between prompts and parameter syntax using two strategies:
|
||||||
|
|
||||||
|
1. **Known prompt matching** -- when the profile has a list of known prompts (`TRK>`, `MOT>`, `DVB>`, etc.), it checks for exact suffix matches. This is the fast path and avoids false positives entirely.
|
||||||
|
|
||||||
|
2. **Bracket filtering** -- for pattern-based detection (when known prompts aren't populated yet), the function rejects any line containing `[` brackets before accepting a `>` match. Parameter syntax like `[<command>]` always appears inside brackets.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _is_prompt_terminated(text: str, profile: DeviceProfile) -> bool:
|
||||||
|
last_line = stripped.split("\n")[-1]
|
||||||
|
|
||||||
|
if profile.prompts:
|
||||||
|
# Check known prompts first (fast path)
|
||||||
|
for p in profile.prompts:
|
||||||
|
if last_stripped.endswith(p):
|
||||||
|
return True
|
||||||
|
# Accept PROMPT_RE match only if no brackets on that line
|
||||||
|
if "[" not in last_line:
|
||||||
|
m = PROMPT_RE.search(last_line)
|
||||||
|
if m:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# No known prompts yet -- fallback to bare > check
|
||||||
|
return stripped.endswith(">")
|
||||||
|
```
|
||||||
|
|
||||||
|
During initial discovery (before any prompts are known), the fallback to `stripped.endswith(">")` is intentionally permissive -- it may occasionally truncate, but it gets the first prompt detected so the more precise logic can take over.
|
||||||
|
|
||||||
|
## ADC scan deadlock (firmware hazard)
|
||||||
|
|
||||||
|
<Aside type="danger" title="This can brick your session">
|
||||||
|
The ADC `scan` command without arguments on an uncalibrated azimuth axis will deadlock the firmware shell. **No serial input can recover it** -- not CR, Ctrl+C, ESC, `q`, or `reboot`. The only recovery is a hardware power cycle.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### What happens
|
||||||
|
|
||||||
|
The ADC submenu's `scan` command performs an azimuth sweep with RSSI readings. When called without explicit position arguments, it reads the current AZ target from the motor controller.
|
||||||
|
|
||||||
|
If the AZ motor has not been homed (which happens when NVS 20 disables the tracker, skipping the boot homing sequence), the position register contains the sentinel value **2147483647** (INT_MAX, or 0x7FFFFFFF).
|
||||||
|
|
||||||
|
The firmware interprets this as a real target position and commands the motor to move there. The motor task blocks on this impossible move, and because the firmware shell is single-threaded -- UART input parsing only happens between command completions -- the shell becomes permanently unresponsive.
|
||||||
|
|
||||||
|
### Why serial input can't help
|
||||||
|
|
||||||
|
The K60's UART4 receive buffer fills up with the bytes you send (CR, `q`, etc.), but the main loop never reads them because it's stuck inside the motor move handler. There is no interrupt-based command abort mechanism in this firmware. The motor task runs to completion (or forever, in this case) before control returns to the shell parser.
|
||||||
|
|
||||||
|
### Mitigation
|
||||||
|
|
||||||
|
- Always home both axes before using ADC `scan`: run `mot` -> `h 0` (AZ) and `h 1` (EL) first
|
||||||
|
- The `birdcage` software never calls ADC `scan` directly
|
||||||
|
- The `console-probe` tool's timeout-based reads will eventually time out, but the firmware shell itself remains dead
|
||||||
|
|
||||||
|
### Other commands affected
|
||||||
|
|
||||||
|
Any command that internally reads motor position and initiates a move could theoretically hit this on an unhomed axis. The `azscanwxp` command in the MOT submenu is similarly dangerous without homing. However, simple position queries (`a` in MOT) safely return the INT_MAX value without attempting a move.
|
||||||
|
|
||||||
|
## Root menu `q` command (firmware design, not a bug)
|
||||||
|
|
||||||
|
The `q` command at the `TRK>` root prompt terminates the firmware shell task. This is by design -- it's a clean shutdown of the command interpreter. But the consequence is identical to the scan deadlock: the console becomes unresponsive and requires a power cycle.
|
||||||
|
|
||||||
|
Inside submenus, `q` safely exits to the parent menu. The hazard is only at the root level.
|
||||||
|
|
||||||
|
The `birdcage` code's `reset_to_root()` method sends `q` to exit submenus, which is safe. But if called when already at root, it would kill the shell. The CarryoutG2Protocol avoids this by using `_send("q")` which reads until the prompt -- if the shell dies, `_send` raises a `TimeoutError` instead of silently losing the connection.
|
||||||
|
|
||||||
|
## `command` false positive in help parsing
|
||||||
|
|
||||||
|
During automated probing, the word `command` appeared as a discovered command in the root menu. This is a false positive extracted from the help text:
|
||||||
|
|
||||||
|
```
|
||||||
|
help [<command>]
|
||||||
|
```
|
||||||
|
|
||||||
|
The help parser saw `<command>` as a valid angle-bracket command name. The fix in `console_probe/discovery.py` maintains a set of known parameter placeholders:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_PARAM_PLACEHOLDERS: set[str] = {
|
||||||
|
"command", "commands", "parameter", "parameters",
|
||||||
|
"value", "values", "index", "name", "arg", "args",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Any word matching this set is rejected during help parsing, preventing it from appearing as a discovered command.
|
||||||
180
src/content/docs/understanding/reverse-engineering.mdx
Normal file
180
src/content/docs/understanding/reverse-engineering.mdx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
title: Reverse Engineering Methodology
|
||||||
|
description: How we mapped the Carryout G2 firmware — from first serial connection to 100+ commands across 12 submenus.
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This page documents the techniques used to reverse-engineer the Winegard Carryout G2 firmware console. The same approach applies to any embedded device with a serial debug console -- the tools and patterns are general-purpose.
|
||||||
|
|
||||||
|
## The seven phases
|
||||||
|
|
||||||
|
The firmware mapping happened in stages, each building on what the previous phase revealed.
|
||||||
|
|
||||||
|
### Phase 1: Serial connection and prompt discovery
|
||||||
|
|
||||||
|
The first step is finding the right electrical interface and baud rate. For the G2, Davidson's winegard-sky-scan project documented RS-422 at 115200 baud, so we didn't have to brute-force it. For an unknown device, you would:
|
||||||
|
|
||||||
|
1. Identify the connector type and pin count (RJ-12 6P6C in this case)
|
||||||
|
2. Use a multimeter to find ground, then probe for differential pairs
|
||||||
|
3. Try common baud rates: 9600, 19200, 38400, 57600, 115200
|
||||||
|
4. Look for readable ASCII in the output at each rate
|
||||||
|
|
||||||
|
Once connected at the right baud rate, send a bare carriage return (`\r`). If the device has a command shell, it will echo a prompt. The G2 responds with `TRK>`.
|
||||||
|
|
||||||
|
<Aside type="tip" title="Polarity matters">
|
||||||
|
RS-422 has separate TX and RX differential pairs. If you get garbled data at the correct baud rate, the RX pair polarity is inverted -- swap the `+` and `-` wires. If you get no response at all, the TX pair polarity is wrong (the dish can't decode your commands, so it silently ignores them).
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Phase 2: Help command and initial inventory
|
||||||
|
|
||||||
|
Nearly every embedded console responds to `?` or `help`. The G2 supports both:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> ?
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints the root menu command list. Each line follows the pattern `Enter <command> - Description`, which gives us both the command name and a hint about whether it enters a submenu.
|
||||||
|
|
||||||
|
From this single command, we got the list of 12 top-level commands (submenus) plus a few root-level commands like `reboot`, `stow`, and `q`.
|
||||||
|
|
||||||
|
### Phase 3: Automated probing with console-probe
|
||||||
|
|
||||||
|
The `?` command only shows what the firmware documents. Many commands exist that aren't listed in help. The `console-probe` tool automates the process of finding them.
|
||||||
|
|
||||||
|
The discovery sequence:
|
||||||
|
|
||||||
|
1. **Detect the prompt** -- send a bare `\r`, read back the response, extract the prompt pattern
|
||||||
|
2. **Detect the error string** -- send a garbage command (`__xyzzy_probe__`), capture the error message. This becomes the filter: any response that contains the error string is "not a valid command"
|
||||||
|
3. **Parse help output** -- extract command names and submenu hints from the `?` response
|
||||||
|
4. **Generate candidates** -- build a list of potential commands: single characters, two-letter combos, common embedded debug commands, and words from external wordlists
|
||||||
|
5. **Probe each candidate** -- send it, check if the response differs from the error string. If it does, record it as a hit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
console-probe --port /dev/ttyUSB2 --baud 115200 --discover-only --json /tmp/discovery.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The probe tool handles recovery from several hazards during probing:
|
||||||
|
|
||||||
|
- **Accidental submenu entry** -- if a command enters a submenu (detected by a prompt change), the tool navigates back to the previous menu level
|
||||||
|
- **Shell termination** -- if a command kills the shell (the `q` command at root), the tool waits for the firmware to restart
|
||||||
|
- **Streaming commands** -- some commands produce continuous output; the timeout-based read strategy handles these by stopping after no new data arrives
|
||||||
|
|
||||||
|
### Phase 4: Interactive submenu exploration
|
||||||
|
|
||||||
|
Automated probing misses commands that require parameters. For example, `a 0 180.0` is a valid motor command, but probing just `a` gives a position readout (which is useful too), and probing `a 0` may return a partial error.
|
||||||
|
|
||||||
|
For each submenu, we entered it manually and typed `?`:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> mot
|
||||||
|
MOT> ?
|
||||||
|
```
|
||||||
|
|
||||||
|
This revealed the full set of 25 MOT commands, including parameter-requiring ones like `a <id> <deg>`, `g <az> <el>`, `pid [motor] [Kp] [Kv] [Ki]`, and `azscanwxp [motor] [span] [resolution] [num_xponders]`.
|
||||||
|
|
||||||
|
The DVB submenu has paginated help -- `?` shows the first page and `man` shows the second. The probe tool tries `man` automatically as an extra help command, but we discovered this through interactive use first.
|
||||||
|
|
||||||
|
Some submenus have commands that only the interactive `?` reveals:
|
||||||
|
|
||||||
|
| Submenu | Commands found by probe | Commands found by `?` only |
|
||||||
|
|---------|------------------------|---------------------------|
|
||||||
|
| MOT | 7 (a, e, l, life, p, r, v) | 18 more (g, h, pid, azscan, azscanwxp, ...) |
|
||||||
|
| DVB | 5 (config, dis, lnbv, nid, snr) | 33 more (rssi, agc, table, send, ...) |
|
||||||
|
| STEP | 4 (e, ma, mv, r) | 3 more (p, pid, v) |
|
||||||
|
|
||||||
|
The total went from ~40 probed hits to over 100 documented commands.
|
||||||
|
|
||||||
|
### Phase 5: NVS dump and configuration space
|
||||||
|
|
||||||
|
The NVS (Non-Volatile Storage) submenu provides `d` to dump all stored values:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> nvs
|
||||||
|
NVS> d
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns every NVS index with its name, current value, saved value, and default value. The dump revealed 133+ configuration entries covering motor limits, PID gains, search behavior, sleep timers, elevation bounds, and satellite configurations.
|
||||||
|
|
||||||
|
Key discoveries from the NVS dump:
|
||||||
|
|
||||||
|
- **Index 20** (`Disable Tracker Proc?`) -- the switch that permanently disables TV satellite search
|
||||||
|
- **Indices 80-88** -- motor velocity and acceleration limits, steps per revolution
|
||||||
|
- **Indices 101-103** -- elevation min/max/home angles
|
||||||
|
- **Indices 128-133** -- PID tuning parameters for the motor control loop
|
||||||
|
|
||||||
|
NVS values can be modified with `e <idx> <value>` and committed with `s`. This is how the tracker is disabled for amateur radio use.
|
||||||
|
|
||||||
|
<Aside type="tip" title="NVS is not EEPROM">
|
||||||
|
The G2 has both an NVS submenu and an EEPROM (EE>) submenu. They are different storage systems. NVS is the firmware's primary configuration store with named parameters. The EEPROM is a lower-level K60 FlexNVM interface that mostly reads as uninitialized (0 or 0x10101). The firmware uses NVS for almost everything.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Phase 6: GPIO probing and hardware mapping
|
||||||
|
|
||||||
|
The GPIO submenu provides direct access to the K60's pin states:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRK> gpio
|
||||||
|
GPIO> regs
|
||||||
|
```
|
||||||
|
|
||||||
|
The `regs` command dumps all GPIO pin states across ports A through E -- 92 pins total. Cross-referencing these with the K60 datasheet's pin multiplexing table identifies which pins are configured for which peripheral (SPI, UART, GPIO, etc.).
|
||||||
|
|
||||||
|
The process:
|
||||||
|
|
||||||
|
1. Run `gpio regs` to get the state of every pin
|
||||||
|
2. Run `gpio dir <pin>` on interesting pins to determine INPUT vs OUTPUT
|
||||||
|
3. Match pin clusters against K60 peripheral assignments from the datasheet
|
||||||
|
4. Correlate with boot log messages (e.g., "SPI1 init @ 4 MHz" tells us which SPI controller connects to the motor drivers)
|
||||||
|
|
||||||
|
This revealed the complete SPI1 (motor drivers) and SPI2 (DVB tuner) pin assignments, the UART4 console pins, and several unidentified outputs that are likely LNB control, status LEDs, and BCM4515 reset.
|
||||||
|
|
||||||
|
### Phase 7: Boot log analysis
|
||||||
|
|
||||||
|
Power-cycling the dish with a serial terminal attached captures the full boot sequence. The G2's boot log is detailed:
|
||||||
|
|
||||||
|
```
|
||||||
|
Bootloader v1.01
|
||||||
|
SPI1 init @ 4 MHz (mode 0x03)
|
||||||
|
Motor init: System=12Inch, master=40000 steps, slave=24960 steps, ratio=1.602564
|
||||||
|
SPI2 init @ 6.857 MHz (mode 0x03)
|
||||||
|
EXTENDED_DVB_DEBUG ENABLED
|
||||||
|
BCM4515 ID 0x4515 Rev B0, FW v113.37, strap 0x25018
|
||||||
|
Auto-search config: blind scan, 18000-24000 ksps, rolloff 0.35
|
||||||
|
Enabled LNB STB
|
||||||
|
Ant ID - 12-IN G2
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line confirms a hardware detail: SPI bus speeds, motor step counts, DVB tuner identification, search parameters. The boot log is the single most information-dense source for understanding the hardware configuration.
|
||||||
|
|
||||||
|
## What console-probe automates vs. what needed manual work
|
||||||
|
|
||||||
|
| Task | Automated | Manual |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| Prompt detection | Yes | -- |
|
||||||
|
| Error string detection | Yes | -- |
|
||||||
|
| Help text parsing | Yes | -- |
|
||||||
|
| Submenu entry/exit | Yes | -- |
|
||||||
|
| Brute-force command probing | Yes | -- |
|
||||||
|
| Paginated help (`man`) | Partially (tries it) | Discovered manually |
|
||||||
|
| Parameter-requiring commands | No | Full manual exploration |
|
||||||
|
| NVS dump interpretation | No | Manual analysis |
|
||||||
|
| GPIO/hardware correlation | No | Cross-reference with datasheet |
|
||||||
|
| Boot log capture | No | Power-cycle with terminal open |
|
||||||
|
| Safety hazard discovery | No | Learned the hard way (ADC `scan` deadlock) |
|
||||||
|
|
||||||
|
The automated tool gets you maybe 40% of the command surface. The remaining 60% requires connecting what the firmware tells you with datasheets, boot logs, and careful experimentation.
|
||||||
|
|
||||||
|
## Lessons learned
|
||||||
|
|
||||||
|
**Prompt-terminated reads are essential.** Fixed-timeout reads miss fast responses and waste time on slow ones. Reading until the `>` prompt character makes every interaction fast and reliable. But you have to handle the edge case where `>` appears inside parameter syntax (e.g., `help [<command>]`).
|
||||||
|
|
||||||
|
**Not every valid command is safe.** The ADC `scan` command without arguments on an unhomed axis targets position 2147483647 and deadlocks the shell forever. The root `q` command kills the firmware shell entirely. A probe tool needs to be cautious about commands that might brick the session.
|
||||||
|
|
||||||
|
**Submenu help reveals more than root help.** The root `?` command gives a one-line summary per submenu, but entering each submenu and typing `?` there reveals the full command set. Some submenus have multi-page help (DVB uses `man` for the second page).
|
||||||
|
|
||||||
|
**NVS is the Rosetta Stone.** The NVS dump gives you named configuration parameters with their defaults and current values. It tells you what the firmware considers configurable, which is a window into what the designers intended the device to do.
|
||||||
|
|
||||||
|
**Boot logs reveal hardware.** The firmware's own initialization messages are the most reliable documentation of what silicon is on the board -- chip IDs, bus speeds, memory sizes, step counts. This is faster and more accurate than trying to identify components visually on a PCB.
|
||||||
80
src/styles/custom.css
Normal file
80
src/styles/custom.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/* Engineering theme — blue-gray palette, monospace code, no purple */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Primary accent: steel blue */
|
||||||
|
--sl-color-accent-low: #1a3a52;
|
||||||
|
--sl-color-accent: #2d5a7b;
|
||||||
|
--sl-color-accent-high: #b8d4e8;
|
||||||
|
|
||||||
|
/* Text hierarchy */
|
||||||
|
--sl-color-white: #e8ecf0;
|
||||||
|
--sl-color-gray-1: #c4cdd6;
|
||||||
|
--sl-color-gray-2: #8fa0b0;
|
||||||
|
--sl-color-gray-3: #5a7080;
|
||||||
|
--sl-color-gray-4: #3a4e5c;
|
||||||
|
--sl-color-gray-5: #2a3a47;
|
||||||
|
--sl-color-gray-6: #1e2c36;
|
||||||
|
--sl-color-black: #141e26;
|
||||||
|
|
||||||
|
/* Code blocks: terminal-inspired */
|
||||||
|
--sl-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
|
||||||
|
|
||||||
|
/* Override any purple that might sneak in */
|
||||||
|
--sl-hue-purple: 205;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode overrides */
|
||||||
|
:root[data-theme='light'] {
|
||||||
|
--sl-color-accent-low: #d6e8f5;
|
||||||
|
--sl-color-accent: #2d5a7b;
|
||||||
|
--sl-color-accent-high: #1a3a52;
|
||||||
|
|
||||||
|
--sl-color-white: #141e26;
|
||||||
|
--sl-color-gray-1: #2a3a47;
|
||||||
|
--sl-color-gray-2: #3a4e5c;
|
||||||
|
--sl-color-gray-3: #5a7080;
|
||||||
|
--sl-color-gray-4: #8fa0b0;
|
||||||
|
--sl-color-gray-5: #c4cdd6;
|
||||||
|
--sl-color-gray-6: #e4eaf0;
|
||||||
|
--sl-color-black: #f5f7f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks get a slightly darker terminal feel */
|
||||||
|
.expressive-code pre {
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger asides for hardware hazards: amber instead of default red */
|
||||||
|
.starlight-aside--danger {
|
||||||
|
--sl-color-asides-text-accent: #d4840a;
|
||||||
|
border-color: #d4840a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Caution asides: warm amber */
|
||||||
|
.starlight-aside--caution {
|
||||||
|
--sl-color-asides-text-accent: #c09020;
|
||||||
|
border-color: #c09020;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tip asides: hardware green */
|
||||||
|
.starlight-aside--tip {
|
||||||
|
--sl-color-asides-text-accent: #2d8a56;
|
||||||
|
border-color: #2d8a56;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling for hardware spec tables */
|
||||||
|
table {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
background-color: var(--sl-color-gray-6);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge in sidebar for "Living" journal */
|
||||||
|
.sl-badge {
|
||||||
|
font-size: 0.7em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user