Skip to content

Known Bugs

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.

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.05travler_rotor.py lines 98-105
  • saveitforparts/Travler-Pro-Rotor — same bug copy-pasted into travler_pro_rotor.py

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

# 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

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

In birdcage/leapfrog.py, the elevation section correctly modifies target_el:

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.

Location: console_probe/serial_io.py, _is_prompt_terminated()

Severity: False prompt detection causes truncated responses

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 _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.

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.

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.

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.

  • 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

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)

Section titled “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.

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:

_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.