Wire firmware-accelerated AZ sweep via azscanwxp

Adds send_with_timeout() to CarryoutG2Protocol for long-running
commands, and az_sweep_firmware() to both SerialBridge and DemoDevice.
Sweep and Sky Map modes now try the firmware path first (single
azscanwxp command, streaming results) and fall back to software
step-dwell-measure on error or when "Software mode" checkbox is
checked. Software sweep fixed to set EL once and move AZ only.
This commit is contained in:
Ryan Malloy 2026-02-14 16:40:53 -07:00
parent ba8859cc31
commit 3cd6424168
4 changed files with 973 additions and 69 deletions

View file

@ -283,6 +283,38 @@ class CarryoutG2Protocol(FirmwareProtocol):
time.sleep(0.001) # brief settle before next command
return resp_data.decode("utf-8", errors="ignore")
def _probe_prompt(self) -> str:
"""Send bare CR and read the prompt string.
Returns the prompt text (e.g. ``'\\r\\nTRK>'`` or ``'\\r\\nMOT>'``).
Used to detect which submenu the firmware is in without sending
a command that could have side effects.
"""
if not self._serial:
raise RuntimeError("Not connected")
self._serial.write(b"\r")
resp = bytearray()
while True:
byte = self._serial.read(1)
if len(byte) == 0:
break # timeout — return what we have
resp.append(byte[0])
if byte[0] == self.PROMPT_CHAR:
break
return resp.decode("utf-8", errors="ignore")
def reset_to_root(self) -> None:
"""Return to TRK> root without killing the shell.
At TRK>, the ``q`` command terminates the UART shell entirely
(requires power cycle). This probes the current prompt first
and only sends ``q`` if we're in a submenu.
"""
prompt = self._probe_prompt()
if "TRK>" in prompt:
return # already at root
self._send("q")
def initialize(self, callback: Callable[[str], None] | None = None) -> None:
"""Prepare G2 for motor commands.
@ -293,22 +325,18 @@ class CarryoutG2Protocol(FirmwareProtocol):
logger.info(
"Initializing Carryout G2 (tracker must be pre-disabled via NVS 20)"
)
self._send("q")
self.reset_to_root()
self.enter_motor_menu()
logger.info("Carryout G2 initialized and ready")
def enter_motor_menu(self) -> None:
self._send("q")
self.reset_to_root()
self._send(self.MOTOR_COMMAND)
def kill_search(self) -> None:
"""No-op — G2 search is disabled permanently via NVS index 20."""
logger.debug("G2 search kill is a no-op (NVS 20 disables tracker)")
def reset_to_root(self) -> None:
"""Return to the firmware root menu."""
self._send("q")
def get_position(self) -> Position:
"""Query dish position.
@ -351,7 +379,7 @@ class CarryoutG2Protocol(FirmwareProtocol):
def enter_dvb_menu(self) -> None:
"""Enter DVB signal analysis submenu (must be at root menu)."""
self._send("q") # ensure root
self.reset_to_root()
self._send("dvb")
def enable_lna(self) -> None:
@ -392,6 +420,21 @@ class CarryoutG2Protocol(FirmwareProtocol):
raise ValueError(f"Could not parse RSSI from: {response!r}")
def send_with_timeout(self, cmd: str, timeout: float = 90) -> str:
"""Send a command with a custom serial timeout.
Used for long-running firmware commands (azscanwxp, azscan) that stream
output over tens of seconds before the final prompt.
"""
if not self._serial:
raise RuntimeError("Not connected")
original = self._serial.timeout
self._serial.timeout = timeout
try:
return self._send(cmd)
finally:
self._serial.timeout = original
def send_raw(self, cmd: str) -> str:
"""Send arbitrary command, return raw prompt-terminated response."""
return self._send(cmd)