Skip to content

console-probe CLI

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.

Terminal window
uv sync # Installs both birdcage and console-probe
Terminal window
console-probe [connection] [discovery overrides] [probing] [output]
Terminal window
# 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"
OptionDefaultDescription
--port/dev/ttyUSB0Serial port
--baud115200Baud rate
--line-endingcrLine ending to send (cr, lf, crlf)
OptionDefaultDescription
--prompt(auto-detect)Override auto-detected root prompt
--error(auto-detect)Override auto-detected error string
--help-cmd?Command to request help
--exit-cmdqCommand to exit submenu
OptionDefaultDescription
--discover-onlyfalseDiscover commands via help only (no brute-force)
--deepfalseProbe all discovered submenus
--submenu(none)Probe a single submenu by name
--timeout0.5Per-command timeout in seconds
--blocklistreboot,stow,def,q,QComma-separated commands to never send
--wordlist(none)Extra candidate words file (repeatable)
--bundled(none)Alias for —wordlist (repeatable)
OptionDefaultDescription
--json(none)Write results as JSON to FILE

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.

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

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

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>)

Everything the tool knows (or detected) about the attached console.

@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]] = {}

A single command parsed from firmware help output.

@dataclass
class HelpEntry:
name: str # command name (lowercase)
description: str # help description text
params: str # parameter syntax, e.g. "[<motor> [angle]]"

The report uses format_version: 2 and includes:

{
"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": [...]
}
}
}
SectionDescription
deviceSerial port and baud rate
detectedAuto-discovered prompt, error string, known commands, submenus
menusPer-submenu structured data: help commands, probe hits, undiscovered commands, stats
resultsLegacy 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.

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.

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.

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.