new gpr
This commit is contained in:
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
|
||||
import numpy as np
|
||||
|
||||
from python_app.hardware_full.librevna_multi_device_driver.cycle_collection import LIBREVNA_NATIVE_SWEEP_TIMEOUT_SECONDS
|
||||
from python_app.hardware_full.librevna_multi_device_driver.exceptions import TransientCollectionError
|
||||
from python_app.hardware_full.librevna_multi_device_driver.models import SweepConfiguration
|
||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||
from python_app.models.run_config_model import RadarSweepModel, RunConfigModel
|
||||
@@ -21,10 +22,11 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Delays applied between successive USB reopen attempts inside recover(). Picked
|
||||
# to give libusb time to re-enumerate a stuck device while staying short enough
|
||||
# that a healthy reconnect feels instant. The total worst-case wait is the sum
|
||||
# of all entries (1.75 s today) plus the cost of close()/open() themselves.
|
||||
# Backoff delays applied only BEFORE each reopen RETRY inside recover(); the first
|
||||
# attempt runs immediately (no upfront sleep) so a transient stall recovers at
|
||||
# once. Picked to give libusb time to re-enumerate a stuck device while staying
|
||||
# short enough that a healthy reconnect feels instant. The total worst-case extra
|
||||
# wait across all retries is the sum of these entries (1.75 s today).
|
||||
_REOPEN_BACKOFF_SECONDS: tuple[float, ...] = (0.25, 0.5, 1.0)
|
||||
|
||||
# Hard ceiling on the wall-clock time a single acquire_collection() may spend in
|
||||
@@ -33,6 +35,14 @@ _REOPEN_BACKOFF_SECONDS: tuple[float, ...] = (0.25, 0.5, 1.0)
|
||||
# systemd TimeoutStopSec so the unit is never SIGKILLed for hanging on exit.
|
||||
_MAX_RECOVERY_WALL_SECONDS: float = 10.0
|
||||
|
||||
# How many times a TRANSIENT collection failure (dropped/incomplete datapoint,
|
||||
# no-progress timeout, or cross-device cycle misalignment) is retried in place —
|
||||
# re-arming the sweep and re-collecting WITHOUT a USB reopen — before escalating
|
||||
# to the heavy close()/reopen recovery. Each retry costs ~one sweep period, so a
|
||||
# handful absorbs ordinary glitches without the multi-second re-enumeration that
|
||||
# previously caused the periodic freeze.
|
||||
_MAX_TRANSIENT_COLLECTION_RETRIES: int = 2
|
||||
|
||||
_INPUT_S_PARAMETERS_BY_OUTPUT: dict[int, tuple[str, ...]] = {
|
||||
0: ("s31", "s41", "s51", "s61"),
|
||||
1: ("s32", "s42", "s52", "s62"),
|
||||
@@ -133,8 +143,12 @@ class MultiDeviceLibreVnaService:
|
||||
return
|
||||
self.close()
|
||||
|
||||
# Try to reopen immediately first, then back off only after a failure: a
|
||||
# transient USB stall usually clears at once, so the common recovery should
|
||||
# not pay an upfront sleep. The backoff delays apply only between retries.
|
||||
reopen_delays = (0.0, *_REOPEN_BACKOFF_SECONDS)
|
||||
last_error: Exception | None = None
|
||||
for attempt_index, delay_s in enumerate(_REOPEN_BACKOFF_SECONDS, start=1):
|
||||
for attempt_index, delay_s in enumerate(reopen_delays, start=1):
|
||||
# Bail out the instant a stop is requested or the recovery budget is
|
||||
# spent, rather than committing to another (re)open attempt.
|
||||
if stop_event is not None and stop_event.is_set():
|
||||
@@ -143,20 +157,21 @@ class MultiDeviceLibreVnaService:
|
||||
if deadline_monotonic is not None and time.monotonic() >= deadline_monotonic:
|
||||
logger.warning("Multi-device recover() aborted: recovery time budget exhausted")
|
||||
break
|
||||
# Interruptible backoff: wait() returns early the moment stop is set.
|
||||
if stop_event is not None:
|
||||
if stop_event.wait(delay_s):
|
||||
logger.info("Multi-device recover() aborted: stop requested")
|
||||
return
|
||||
else:
|
||||
time.sleep(delay_s)
|
||||
# Interruptible backoff before retries; the first attempt has no delay.
|
||||
if delay_s > 0.0:
|
||||
if stop_event is not None:
|
||||
if stop_event.wait(delay_s):
|
||||
logger.info("Multi-device recover() aborted: stop requested")
|
||||
return
|
||||
else:
|
||||
time.sleep(delay_s)
|
||||
try:
|
||||
self.open()
|
||||
if self._controller is not None:
|
||||
logger.info(
|
||||
"Multi-device reopen succeeded on attempt %d/%d (after %.2fs)",
|
||||
attempt_index,
|
||||
len(_REOPEN_BACKOFF_SECONDS),
|
||||
len(reopen_delays),
|
||||
delay_s,
|
||||
)
|
||||
return
|
||||
@@ -165,7 +180,7 @@ class MultiDeviceLibreVnaService:
|
||||
logger.warning(
|
||||
"Multi-device reopen attempt %d/%d failed after %.2fs: %s",
|
||||
attempt_index,
|
||||
len(_REOPEN_BACKOFF_SECONDS),
|
||||
len(reopen_delays),
|
||||
delay_s,
|
||||
exc,
|
||||
)
|
||||
@@ -230,7 +245,9 @@ class MultiDeviceLibreVnaService:
|
||||
recovery_deadline = time.monotonic() + _MAX_RECOVERY_WALL_SECONDS
|
||||
for attempt_index in range(self.recovery_attempts + 1):
|
||||
try:
|
||||
return self._acquire_native_collection(collection_id, capture_start_ns)
|
||||
return self._acquire_native_collection_with_transient_retries(
|
||||
collection_id, capture_start_ns, stop_event=stop_event
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last_error = exc
|
||||
if attempt_index >= self.recovery_attempts:
|
||||
@@ -272,6 +289,43 @@ class MultiDeviceLibreVnaService:
|
||||
assert last_error is not None
|
||||
raise last_error
|
||||
|
||||
def _acquire_native_collection_with_transient_retries(
|
||||
self,
|
||||
collection_id: int,
|
||||
capture_start_ns: int,
|
||||
*,
|
||||
stop_event: threading.Event | None = None,
|
||||
) -> SweepCollection:
|
||||
"""Acquire one collection, retrying device-healthy failures in place.
|
||||
|
||||
A ``TransientCollectionError`` (dropped/incomplete datapoint, no-progress
|
||||
timeout, or cross-device cycle misalignment) leaves the USB transports
|
||||
alive, so it is recovered by simply re-collecting: the controller auto-idled
|
||||
on failure, so the next ``configure_continuous_sweep`` re-sends
|
||||
``SWEEP_SETTINGS`` (re-arm). This costs ~one sweep period instead of the
|
||||
multi-second ``close()``/reopen that genuine transport death requires.
|
||||
Non-transient errors propagate immediately to the reopen-based recovery.
|
||||
"""
|
||||
transient_error: TransientCollectionError | None = None
|
||||
for transient_attempt in range(_MAX_TRANSIENT_COLLECTION_RETRIES + 1):
|
||||
try:
|
||||
return self._acquire_native_collection(collection_id, capture_start_ns)
|
||||
except TransientCollectionError as exc:
|
||||
transient_error = exc
|
||||
if transient_attempt >= _MAX_TRANSIENT_COLLECTION_RETRIES:
|
||||
raise
|
||||
if stop_event is not None and stop_event.is_set():
|
||||
raise
|
||||
logger.debug(
|
||||
"transient multi-device collection error (%d/%d); re-arming and re-collecting "
|
||||
"without USB reopen: %s",
|
||||
transient_attempt + 1,
|
||||
_MAX_TRANSIENT_COLLECTION_RETRIES,
|
||||
exc,
|
||||
)
|
||||
assert transient_error is not None # loop either returns or raises
|
||||
raise transient_error
|
||||
|
||||
def _acquire_native_collection(self, collection_id: int, capture_start_ns: int) -> SweepCollection:
|
||||
if self._controller is None:
|
||||
raise RuntimeError("Multi-device controller is not open")
|
||||
|
||||
Reference in New Issue
Block a user