some changes and log fix

This commit is contained in:
2026-07-30 19:58:53 +03:00
parent 68bec25f17
commit 7c6cab07fc
10 changed files with 228 additions and 61 deletions
@@ -131,7 +131,14 @@ class MultiDeviceVnaController:
if not self._reference_configuration_applied:
self._configure_reference_clocks()
self._drain_all_received_packets()
drain_started_seconds = time.monotonic()
drained_packet_count = self._drain_all_received_packets()
logger.debug(
"timing: drain discarded %d stale packet(s) in %.2f ms (t=%.1f ms)",
drained_packet_count,
(time.monotonic() - drain_started_seconds) * 1e3,
time.monotonic() * 1e3,
)
if (
self._sweep_is_running
@@ -334,12 +341,16 @@ class MultiDeviceVnaController:
self._sweep_is_running = True
logger.debug("Sweep settings applied to all devices; sweep running")
def _drain_all_received_packets(self) -> None:
def _drain_all_received_packets(self) -> int:
"""Empty every device's received-packet queue, in parallel for 2+ devices.
Concurrent draining keeps cross-device timing skew small so a hardware
cycle wrap cannot slip between per-device drains and desynchronize the
cycle counters.
Returns the total number of discarded packets, which the caller logs: a large
count means the host was far behind the free-running stream, a near-zero count
means the drain landed right after a sweep boundary.
"""
# Drain every device queue in parallel rather than one after another:
# serial drain leaves up to a few hundred microseconds of skew between
@@ -349,22 +360,31 @@ class MultiDeviceVnaController:
# so concurrent get_nowait calls do not contend. A single device case
# just runs inline to avoid the thread-spawn overhead.
if len(self._all_devices) < 2:
for device_connection in self._all_devices:
device_connection.drain_received_packets()
return
return sum(
len(device_connection.drain_received_packets())
for device_connection in self._all_devices
)
drained_counts = [0] * len(self._all_devices)
def drain_one_device(device_index: int, device_connection: LibreVnaUsbBulkConnection) -> None:
"""Drain one device's queue and record how many packets it held."""
drained_counts[device_index] = len(device_connection.drain_received_packets())
drain_threads = [
threading.Thread(
target=device_connection.drain_received_packets,
target=drain_one_device,
args=(device_index, device_connection),
name=f"drain-{device_connection.serial_number}",
daemon=True,
)
for device_connection in self._all_devices
for device_index, device_connection in enumerate(self._all_devices)
]
for drain_thread in drain_threads:
drain_thread.start()
for drain_thread in drain_threads:
drain_thread.join()
return sum(drained_counts)
@staticmethod
def _normalize_master_stimulus_ports(master_stimulus_ports: Sequence[int]) -> tuple[int, ...]:
@@ -441,6 +441,7 @@ def collect_complete_running_sweep_cycles(
def build_cycle_tracking_handler(
cycle_aware_handler: Callable[[ParsedVnaDatapoint, int], None],
device_state: _DeviceCollectionState,
device_label: str = "device",
) -> Callable[[ParsedVnaDatapoint], bool]:
"""Wrap a cycle-aware handler with cross-device cycle tracking.
@@ -462,6 +463,11 @@ def collect_complete_running_sweep_cycles(
cycle_tracking_state = {
"current_cycle_index": 0,
"synchronized": False,
# How many mid-sweep points were thrown away before the anchor was found.
# Near zero means the drain landed on a sweep boundary — the case where a
# stale point 0 could still have been in flight; a large count means the
# remainder of the in-progress sweep was safely skipped.
"pre_anchor_skipped": 0,
}
def handle_datapoint(parsed_datapoint: ParsedVnaDatapoint) -> bool:
@@ -475,6 +481,7 @@ def collect_complete_running_sweep_cycles(
if not cycle_tracking_state["synchronized"]:
if current_point_index != 0:
cycle_tracking_state["pre_anchor_skipped"] += 1
return False
# Candidate cycle 0. Commit it only once every device confirms it
# observed point 0 of the SAME physical sweep; otherwise reject the
@@ -484,6 +491,14 @@ def collect_complete_running_sweep_cycles(
report_cycle_misalignment()
return False
cycle_tracking_state["synchronized"] = True
logger.debug(
"timing: %s anchored cycle 0 after skipping %d mid-sweep point(s) of %d "
"(t=%.1f ms)",
device_label,
cycle_tracking_state["pre_anchor_skipped"],
point_count,
time.monotonic() * 1e3,
)
cycle_aware_handler(parsed_datapoint, 0)
return True
@@ -493,6 +508,12 @@ def collect_complete_running_sweep_cycles(
# spurious wrap and desynchronize the cycle counter.
if current_point_index == 0:
cycle_tracking_state["current_cycle_index"] += 1
logger.debug(
"timing: %s first point of NEXT sweep arrived (cycle -> %d, t=%.1f ms)",
device_label,
cycle_tracking_state["current_cycle_index"],
time.monotonic() * 1e3,
)
current_cycle_index = cycle_tracking_state["current_cycle_index"]
if current_cycle_index >= cycle_count:
# The sweep just wrapped past the final requested cycle, closing its
@@ -505,6 +526,14 @@ def collect_complete_running_sweep_cycles(
return False
cycle_aware_handler(parsed_datapoint, current_cycle_index)
if current_point_index == point_count - 1:
logger.debug(
"timing: %s last point of cycle %d arrived (index=%d, t=%.1f ms)",
device_label,
current_cycle_index,
current_point_index,
time.monotonic() * 1e3,
)
return True
return handle_datapoint
@@ -587,7 +616,9 @@ def collect_complete_running_sweep_cycles(
point_index,
] = port_receiver_value
return build_cycle_tracking_handler(handle_slave_datapoint, device_state)
return build_cycle_tracking_handler(
handle_slave_datapoint, device_state, device_label=f"slave{slave_index}"
)
master_device_state = _DeviceCollectionState()
collection_threads = [
@@ -595,7 +626,9 @@ def collect_complete_running_sweep_cycles(
target=collect_datapoints_from_device,
args=(
master_device_connection,
build_cycle_tracking_handler(handle_master_datapoint, master_device_state),
build_cycle_tracking_handler(
handle_master_datapoint, master_device_state, device_label="master"
),
master_device_state,
),
daemon=True,