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:
parent
ba8859cc31
commit
3cd6424168
4 changed files with 973 additions and 69 deletions
|
|
@ -66,6 +66,32 @@ class SerialBridge:
|
|||
self._menu = Menu.UNKNOWN
|
||||
self._connected = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Menu prompt → string mapping for status display
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
_MENU_PROMPTS: dict[Menu, str] = {
|
||||
Menu.ROOT: "TRK>",
|
||||
Menu.MOT: "MOT>",
|
||||
Menu.DVB: "DVB>",
|
||||
Menu.NVS: "NVS>",
|
||||
Menu.A3981: "A3981>",
|
||||
Menu.ADC: "ADC>",
|
||||
Menu.OS: "OS>",
|
||||
Menu.STEP: "STEP>",
|
||||
Menu.PEAK: "PEAK>",
|
||||
Menu.EEPROM: "EE>",
|
||||
Menu.GPIO: "GPIO>",
|
||||
Menu.LATLON: "LATLON>",
|
||||
Menu.DIPSWITCH: "DIPSWITCH>",
|
||||
Menu.UNKNOWN: "???",
|
||||
}
|
||||
|
||||
@property
|
||||
def current_menu(self) -> str:
|
||||
"""Current firmware prompt string for status display."""
|
||||
return self._MENU_PROMPTS.get(self._menu, "???")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -82,6 +108,21 @@ class SerialBridge:
|
|||
self._proto.reset_to_root()
|
||||
self._menu = Menu.ROOT
|
||||
|
||||
def _detect_menu(self) -> Menu:
|
||||
"""Probe firmware prompt and return the corresponding Menu enum.
|
||||
|
||||
Caller must hold ``_lock``.
|
||||
"""
|
||||
prompt = self._proto._probe_prompt()
|
||||
upper = prompt.upper()
|
||||
# Check against known prompt strings (handles EE> vs EEPROM>, etc.)
|
||||
for menu, prompt_str in self._MENU_PROMPTS.items():
|
||||
if menu == Menu.UNKNOWN:
|
||||
continue
|
||||
if prompt_str.upper() in upper:
|
||||
return menu
|
||||
return Menu.UNKNOWN
|
||||
|
||||
def _ensure_menu(self, target: Menu) -> None:
|
||||
"""Navigate to *target* submenu if not already there.
|
||||
|
||||
|
|
@ -114,7 +155,7 @@ class SerialBridge:
|
|||
with self._lock:
|
||||
self._proto.connect(port, baudrate)
|
||||
self._connected = True
|
||||
self._menu = Menu.UNKNOWN
|
||||
self._menu = self._detect_menu()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Close the serial connection."""
|
||||
|
|
@ -139,7 +180,8 @@ class SerialBridge:
|
|||
with self._lock:
|
||||
if not skip_init:
|
||||
self._proto.initialize()
|
||||
self._menu = Menu.MOT # initialize() ends in MOT>
|
||||
self._menu = Menu.MOT # initialize() ends in MOT>
|
||||
# else: leave _menu as whatever connect() detected
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Motor (MOT>)
|
||||
|
|
@ -296,6 +338,116 @@ class SerialBridge:
|
|||
|
||||
return result
|
||||
|
||||
def get_pid_gains(self) -> dict[str, dict[str, float]]:
|
||||
"""Read PID gains for both motor axes.
|
||||
|
||||
Firmware returns: ``Kp=600 Kv=60 Ki=1`` per motor.
|
||||
|
||||
Returns:
|
||||
``{"az": {"kp": 600, "kv": 60, "ki": 1},
|
||||
"el": {"kp": 250, "kv": 50, "ki": 1}}``
|
||||
"""
|
||||
with self._lock:
|
||||
self._ensure_menu(Menu.MOT)
|
||||
response = self._send("pid")
|
||||
|
||||
# Parse "Kp=600 Kv=60 Ki=1" patterns. The pid command without args
|
||||
# shows both motors. We look for two sets of Kp/Kv/Ki values.
|
||||
kp_matches = re.findall(r"Kp[=:]?\s*(\d+)", response)
|
||||
kv_matches = re.findall(r"Kv[=:]?\s*(\d+)", response)
|
||||
ki_matches = re.findall(r"Ki[=:]?\s*(\d+)", response)
|
||||
|
||||
result = {
|
||||
"az": {"kp": 600.0, "kv": 60.0, "ki": 1.0},
|
||||
"el": {"kp": 250.0, "kv": 50.0, "ki": 1.0},
|
||||
}
|
||||
|
||||
if len(kp_matches) >= 2:
|
||||
result["az"]["kp"] = float(kp_matches[0])
|
||||
result["el"]["kp"] = float(kp_matches[1])
|
||||
elif len(kp_matches) == 1:
|
||||
result["az"]["kp"] = float(kp_matches[0])
|
||||
|
||||
if len(kv_matches) >= 2:
|
||||
result["az"]["kv"] = float(kv_matches[0])
|
||||
result["el"]["kv"] = float(kv_matches[1])
|
||||
elif len(kv_matches) == 1:
|
||||
result["az"]["kv"] = float(kv_matches[0])
|
||||
|
||||
if len(ki_matches) >= 2:
|
||||
result["az"]["ki"] = float(ki_matches[0])
|
||||
result["el"]["ki"] = float(ki_matches[1])
|
||||
elif len(ki_matches) == 1:
|
||||
result["az"]["ki"] = float(ki_matches[0])
|
||||
|
||||
return result
|
||||
|
||||
def set_pid_gains(self, motor_id: int, kp: float, kv: float, ki: float) -> None:
|
||||
"""Write PID gains for a single motor axis.
|
||||
|
||||
Args:
|
||||
motor_id: 0 for AZ, 1 for EL.
|
||||
kp: Proportional gain.
|
||||
kv: Velocity gain.
|
||||
ki: Integral gain.
|
||||
"""
|
||||
with self._lock:
|
||||
self._ensure_menu(Menu.MOT)
|
||||
self._send(f"pid {motor_id} {int(kp)} {int(kv)} {int(ki)}")
|
||||
|
||||
def az_sweep_firmware(
|
||||
self,
|
||||
start_az: float,
|
||||
span: float,
|
||||
step_cdeg: int,
|
||||
num_xponders: int,
|
||||
timeout: float = 120,
|
||||
) -> list[dict[str, float]]:
|
||||
"""Execute a firmware-accelerated AZ sweep via azscanwxp.
|
||||
|
||||
Moves to *start_az* first, then runs the firmware sweep command which
|
||||
handles motor movement and RSSI measurement atomically — no per-point
|
||||
serial round-trips.
|
||||
|
||||
Args:
|
||||
start_az: Starting azimuth in degrees.
|
||||
span: Total sweep width in degrees.
|
||||
step_cdeg: Step size in centidegrees (100 = 1.00°).
|
||||
num_xponders: Number of transponders to cycle per position.
|
||||
timeout: Serial read timeout for the long-running command.
|
||||
|
||||
Returns:
|
||||
List of dicts with keys: az, rssi, lock, snr.
|
||||
"""
|
||||
with self._lock:
|
||||
self._ensure_menu(Menu.MOT)
|
||||
# Move to start position and wait for prompt.
|
||||
self._send(f"a 0 {start_az}")
|
||||
# Execute firmware sweep with extended timeout.
|
||||
response = self._proto.send_with_timeout(
|
||||
f"azscanwxp 0 {span} {step_cdeg} {num_xponders}",
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# Parse streaming output lines.
|
||||
# Motor:<id> Angle:<cdeg> RSSI:<adc> Lock:<0/1> SNR:<dB>
|
||||
results: list[dict[str, float]] = []
|
||||
for match in re.finditer(
|
||||
r"Angle:(-?\d+)\s+RSSI:(\d+)\s+Lock:(\d)\s+SNR:(-?\d+\.?\d*)",
|
||||
response,
|
||||
):
|
||||
results.append(
|
||||
{
|
||||
"az": int(match.group(1)) / 100.0,
|
||||
"rssi": float(match.group(2)),
|
||||
"lock": float(match.group(3)),
|
||||
"snr": float(match.group(4)),
|
||||
}
|
||||
)
|
||||
|
||||
logger.info("Firmware sweep returned %d points", len(results))
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Signal (DVB>)
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue