fix update
This commit is contained in:
@ -49,12 +49,14 @@ from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo, SweepPacket
|
||||
RAW_PLOT_MAX_POINTS = 4096
|
||||
RAW_WATERFALL_MAX_POINTS = 2048
|
||||
BSCAN_MAX_POINTS = 512
|
||||
UI_QUEUE_MAXSIZE = 128
|
||||
UI_MAX_PACKETS_PER_TICK = 8
|
||||
DEBUG_FRAME_LOG_EVERY = 10
|
||||
UI_BACKLOG_TAIL_THRESHOLD_MULTIPLIER = 1
|
||||
UI_BACKLOG_LATEST_ONLY_THRESHOLD_MULTIPLIER = 2
|
||||
UI_HEAVY_REFRESH_BACKLOG_MULTIPLIER = 1
|
||||
UI_HEAVY_REFRESH_MAX_STRIDE = 4
|
||||
UI_AXIS_REFRESH_INTERVAL_S = 0.35
|
||||
UI_DATA_WAIT_NOTE_AFTER_S = 3.0
|
||||
FFT_LOW_CUT_SLIDER_SCALE = 10
|
||||
FFT_LOW_CUT_MAX_PERCENT = 99.0
|
||||
@ -804,7 +806,7 @@ def run_pyqtgraph(args) -> None:
|
||||
"pyqtgraph и совместимый Qt backend не найдены. Установите: pip install pyqtgraph PyQt5"
|
||||
) from exc
|
||||
|
||||
queue: Queue[SweepPacket] = Queue(maxsize=1000)
|
||||
queue: Queue[SweepPacket] = Queue(maxsize=UI_QUEUE_MAXSIZE)
|
||||
stop_event = threading.Event()
|
||||
reader = SweepReader(
|
||||
args.port,
|
||||
@ -1220,6 +1222,100 @@ def run_pyqtgraph(args) -> None:
|
||||
expected_sweep_width = 0
|
||||
base_freqs_cache: Dict[int, np.ndarray] = {}
|
||||
last_packet_processed_at: Optional[float] = None
|
||||
axis_range_cache: Dict[str, Tuple[float, ...]] = {}
|
||||
image_rect_cache: Dict[str, Tuple[float, ...]] = {}
|
||||
last_signal_mode_signature: Optional[Tuple[Optional[str], bool, bool]] = None
|
||||
last_fft_low_cut_label_text: Optional[str] = None
|
||||
|
||||
def finite_range_pair(lower: float, upper: float) -> Optional[Tuple[float, float]]:
|
||||
try:
|
||||
lower_val = float(lower)
|
||||
upper_val = float(upper)
|
||||
except Exception:
|
||||
return None
|
||||
if not (np.isfinite(lower_val) and np.isfinite(upper_val)):
|
||||
return None
|
||||
if upper_val <= lower_val:
|
||||
upper_val = lower_val + 1e-9
|
||||
return (lower_val, upper_val)
|
||||
|
||||
def cached_tuple_changed(cache: Dict[str, Tuple[float, ...]], key: str, values: Tuple[float, ...]) -> bool:
|
||||
previous = cache.get(key)
|
||||
if previous is None or len(previous) != len(values):
|
||||
return True
|
||||
return not bool(np.allclose(previous, values, rtol=1e-6, atol=1e-9))
|
||||
|
||||
def set_x_range_if_changed(key: str, plot, lower: float, upper: float, *, padding: float = 0.0) -> bool:
|
||||
bounds = finite_range_pair(lower, upper)
|
||||
if bounds is None:
|
||||
return False
|
||||
values = (bounds[0], bounds[1])
|
||||
if not cached_tuple_changed(axis_range_cache, key, values):
|
||||
return False
|
||||
try:
|
||||
plot.setXRange(bounds[0], bounds[1], padding=padding)
|
||||
except Exception:
|
||||
return False
|
||||
axis_range_cache[key] = values
|
||||
return True
|
||||
|
||||
def set_y_range_if_changed(key: str, plot, lower: float, upper: float, *, padding: float = 0.0) -> bool:
|
||||
bounds = finite_range_pair(lower, upper)
|
||||
if bounds is None:
|
||||
return False
|
||||
values = (bounds[0], bounds[1])
|
||||
if not cached_tuple_changed(axis_range_cache, key, values):
|
||||
return False
|
||||
try:
|
||||
plot.setYRange(bounds[0], bounds[1], padding=padding)
|
||||
except Exception:
|
||||
return False
|
||||
axis_range_cache[key] = values
|
||||
return True
|
||||
|
||||
def set_xy_range_if_changed(
|
||||
key: str,
|
||||
plot,
|
||||
*,
|
||||
x_bounds: Tuple[float, float],
|
||||
y_bounds: Tuple[float, float],
|
||||
padding: float = 0.0,
|
||||
) -> bool:
|
||||
x_range = finite_range_pair(x_bounds[0], x_bounds[1])
|
||||
y_range = finite_range_pair(y_bounds[0], y_bounds[1])
|
||||
if x_range is None or y_range is None:
|
||||
return False
|
||||
values = (x_range[0], x_range[1], y_range[0], y_range[1])
|
||||
if not cached_tuple_changed(axis_range_cache, key, values):
|
||||
return False
|
||||
try:
|
||||
plot.setRange(xRange=x_range, yRange=y_range, padding=padding)
|
||||
except Exception:
|
||||
return False
|
||||
axis_range_cache[key] = values
|
||||
return True
|
||||
|
||||
def set_image_rect_if_changed(key: str, image_item, x: float, y: float, width: float, height: float) -> bool:
|
||||
try:
|
||||
values = (float(x), float(y), float(width), float(height))
|
||||
except Exception:
|
||||
return False
|
||||
if not (
|
||||
np.isfinite(values[0])
|
||||
and np.isfinite(values[1])
|
||||
and np.isfinite(values[2])
|
||||
and np.isfinite(values[3])
|
||||
and values[2] > 0.0
|
||||
and values[3] > 0.0
|
||||
):
|
||||
return False
|
||||
if not cached_tuple_changed(image_rect_cache, key, values):
|
||||
return False
|
||||
if not set_image_rect_if_ready(image_item, values[0], values[1], values[2], values[3]):
|
||||
return False
|
||||
image_rect_cache[key] = values
|
||||
return True
|
||||
|
||||
fixed_ylim: Optional[Tuple[float, float]] = None
|
||||
if args.ylim:
|
||||
try:
|
||||
@ -1228,7 +1324,7 @@ def run_pyqtgraph(args) -> None:
|
||||
except Exception:
|
||||
fixed_ylim = None
|
||||
if fixed_ylim is not None:
|
||||
p_line.setYRange(fixed_ylim[0], fixed_ylim[1], padding=0)
|
||||
set_y_range_if_changed("line_y", p_line, fixed_ylim[0], fixed_ylim[1], padding=0)
|
||||
|
||||
def ensure_buffer(sweep_width: int) -> None:
|
||||
changed = runtime.ring.ensure_init(sweep_width)
|
||||
@ -1242,15 +1338,27 @@ def run_pyqtgraph(args) -> None:
|
||||
disp_raw = sanitize_image_for_display(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS))
|
||||
if disp_raw is not None:
|
||||
img.setImage(disp_raw, autoLevels=False)
|
||||
set_image_rect_if_ready(img, 0.0, f_min, float(max_sweeps), max(1e-9, f_max - f_min))
|
||||
p_img.setRange(xRange=(0, max_sweeps - 1), yRange=(f_min, f_max), padding=0)
|
||||
p_line.setXRange(f_min, f_max, padding=0)
|
||||
set_image_rect_if_changed("raw_waterfall_rect", img, 0.0, f_min, float(max_sweeps), max(1e-9, f_max - f_min))
|
||||
set_xy_range_if_changed(
|
||||
"raw_waterfall_range",
|
||||
p_img,
|
||||
x_bounds=(0, max_sweeps - 1),
|
||||
y_bounds=(f_min, f_max),
|
||||
padding=0,
|
||||
)
|
||||
set_x_range_if_changed("line_x", p_line, f_min, f_max, padding=0)
|
||||
disp_fft = sanitize_image_for_display(runtime.ring.get_display_fft_linear())
|
||||
if disp_fft is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False)
|
||||
set_image_rect_if_ready(img_fft, 0.0, 0.0, float(max_sweeps), 1.0)
|
||||
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(0.0, 1.0), padding=0)
|
||||
p_fft.setXRange(0.0, 1.0, padding=0)
|
||||
set_image_rect_if_changed("fft_waterfall_rect", img_fft, 0.0, 0.0, float(max_sweeps), 1.0)
|
||||
set_xy_range_if_changed(
|
||||
"fft_waterfall_range",
|
||||
p_spec,
|
||||
x_bounds=(0, max_sweeps - 1),
|
||||
y_bounds=(0.0, 1.0),
|
||||
padding=0,
|
||||
)
|
||||
set_x_range_if_changed("fft_x", p_fft, 0.0, 1.0, padding=0)
|
||||
|
||||
def _active_distance_axis() -> Optional[np.ndarray]:
|
||||
if runtime.current_distances is not None and runtime.current_distances.size > 0:
|
||||
@ -1261,12 +1369,16 @@ def run_pyqtgraph(args) -> None:
|
||||
return resolve_distance_cut_start(_active_distance_axis(), fft_low_cut_percent)
|
||||
|
||||
def refresh_fft_low_cut_label() -> None:
|
||||
nonlocal last_fft_low_cut_label_text
|
||||
text = f"{fft_low_cut_percent:.1f}%"
|
||||
cut_start = _active_distance_cut_start()
|
||||
if cut_start is not None and np.isfinite(cut_start):
|
||||
text = f"{text} (~{cut_start:.4g} м)"
|
||||
if text == last_fft_low_cut_label_text:
|
||||
return
|
||||
try:
|
||||
fft_low_cut_value_label.setText(text)
|
||||
last_fft_low_cut_label_text = text
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -1278,9 +1390,15 @@ def run_pyqtgraph(args) -> None:
|
||||
)
|
||||
if freq_bounds is not None:
|
||||
f_min, f_max = freq_bounds
|
||||
set_image_rect_if_ready(img, 0.0, f_min, float(max_sweeps), f_max - f_min)
|
||||
p_img.setRange(xRange=(0, max_sweeps - 1), yRange=(f_min, f_max), padding=0)
|
||||
p_line.setXRange(f_min, f_max, padding=0)
|
||||
set_image_rect_if_changed("raw_waterfall_rect", img, 0.0, f_min, float(max_sweeps), f_max - f_min)
|
||||
set_xy_range_if_changed(
|
||||
"raw_waterfall_range",
|
||||
p_img,
|
||||
x_bounds=(0, max_sweeps - 1),
|
||||
y_bounds=(f_min, f_max),
|
||||
padding=0,
|
||||
)
|
||||
set_x_range_if_changed("line_x", p_line, f_min, f_max, padding=0)
|
||||
|
||||
distance_bounds = resolve_axis_bounds(runtime.ring.distance_axis)
|
||||
if distance_bounds is not None:
|
||||
@ -1289,9 +1407,15 @@ def run_pyqtgraph(args) -> None:
|
||||
if d_cut is not None and np.isfinite(d_cut):
|
||||
d_min = max(float(d_min), float(d_cut))
|
||||
span = max(1e-9, float(d_max - d_min))
|
||||
set_image_rect_if_ready(img_fft, 0.0, d_min, float(max_sweeps), span)
|
||||
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(d_min, d_max), padding=0)
|
||||
p_fft.setXRange(d_min, d_max, padding=0)
|
||||
set_image_rect_if_changed("fft_waterfall_rect", img_fft, 0.0, d_min, float(max_sweeps), span)
|
||||
set_xy_range_if_changed(
|
||||
"fft_waterfall_range",
|
||||
p_spec,
|
||||
x_bounds=(0, max_sweeps - 1),
|
||||
y_bounds=(d_min, d_max),
|
||||
padding=0,
|
||||
)
|
||||
set_x_range_if_changed("fft_x", p_fft, d_min, d_max, padding=0)
|
||||
refresh_fft_low_cut_label()
|
||||
|
||||
def resolve_curve_xs(size: int) -> np.ndarray:
|
||||
@ -1390,12 +1514,16 @@ def run_pyqtgraph(args) -> None:
|
||||
signal_kind = get_signal_kind()
|
||||
return bool(complex_sweep_mode) and signal_kind not in {"bin_logdet", "bin_iq_do1_tagged"}
|
||||
|
||||
def refresh_signal_mode_labels() -> None:
|
||||
def refresh_signal_mode_labels(*, force: bool = False) -> None:
|
||||
nonlocal last_signal_mode_signature
|
||||
signal_kind = get_signal_kind()
|
||||
active_complex = current_packet_is_complex()
|
||||
is_logdet = signal_kind == "bin_logdet"
|
||||
is_bin_iq = signal_kind == "bin_iq"
|
||||
is_do1_tagged = signal_kind == "bin_iq_do1_tagged"
|
||||
signature = (signal_kind, bool(active_complex), bool(is_do1_tagged))
|
||||
if (not force) and signature == last_signal_mode_signature:
|
||||
return
|
||||
|
||||
try:
|
||||
if is_logdet:
|
||||
@ -1428,6 +1556,7 @@ def run_pyqtgraph(args) -> None:
|
||||
p_fft.setVisible(not is_do1_tagged)
|
||||
p_spec.setVisible(not is_do1_tagged)
|
||||
p_complex_calib.setVisible((not is_do1_tagged) and bool(active_complex))
|
||||
last_signal_mode_signature = signature
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -1544,6 +1673,8 @@ def run_pyqtgraph(args) -> None:
|
||||
|
||||
def reset_ring_buffers() -> None:
|
||||
runtime.ring.reset()
|
||||
axis_range_cache.clear()
|
||||
image_rect_cache.clear()
|
||||
runtime.current_distances = None
|
||||
runtime.current_fft_complex = None
|
||||
runtime.current_fft_mag = None
|
||||
@ -2135,7 +2266,7 @@ def run_pyqtgraph(args) -> None:
|
||||
set_fft_curve_visibility()
|
||||
set_fft_mode()
|
||||
set_fft_low_cut_percent()
|
||||
refresh_signal_mode_labels()
|
||||
refresh_signal_mode_labels(force=True)
|
||||
|
||||
try:
|
||||
range_min_spin.valueChanged.connect(lambda _v: set_working_range())
|
||||
@ -2324,6 +2455,7 @@ def run_pyqtgraph(args) -> None:
|
||||
ui_frames_skipped = 0
|
||||
ui_started_at = time.perf_counter()
|
||||
update_ticks = 0
|
||||
last_axis_range_refresh_at = 0.0
|
||||
|
||||
def refresh_current_fft_cache(sweep_for_fft: np.ndarray, bins: int) -> None:
|
||||
fft_complex = compute_fft_complex_row(
|
||||
@ -2521,7 +2653,6 @@ def run_pyqtgraph(args) -> None:
|
||||
runtime.full_current_aux_curves = None
|
||||
runtime.full_current_aux_curves_codes = None
|
||||
runtime.full_current_sweep_codes = None
|
||||
refresh_signal_mode_labels()
|
||||
refresh_current_window(push_to_ring=True)
|
||||
processed_frames += 1
|
||||
last_packet_processed_at = time.time()
|
||||
@ -2558,7 +2689,7 @@ def run_pyqtgraph(args) -> None:
|
||||
pass
|
||||
|
||||
def update() -> None:
|
||||
nonlocal peak_ref_window, status_dirty, update_ticks
|
||||
nonlocal peak_ref_window, status_dirty, update_ticks, last_axis_range_refresh_at
|
||||
norm_display_scale = 500.0
|
||||
if peak_calibrate_mode and any(edit.hasFocus() for edit in c_edits):
|
||||
return
|
||||
@ -2568,8 +2699,14 @@ def run_pyqtgraph(args) -> None:
|
||||
clear_expired_status_note()
|
||||
refresh_waiting_data_note()
|
||||
|
||||
now_perf = time.perf_counter()
|
||||
changed = drain_queue() > 0
|
||||
redraw_needed = changed or runtime.plot_dirty
|
||||
refresh_auto_ranges = bool(runtime.plot_dirty) or (
|
||||
redraw_needed and (now_perf - last_axis_range_refresh_at) >= UI_AXIS_REFRESH_INTERVAL_S
|
||||
)
|
||||
if refresh_auto_ranges:
|
||||
last_axis_range_refresh_at = now_perf
|
||||
refresh_heavy_views = (
|
||||
runtime.plot_dirty
|
||||
or last_heavy_refresh_stride <= 1
|
||||
@ -2743,8 +2880,8 @@ def run_pyqtgraph(args) -> None:
|
||||
(runtime.current_sweep_norm * norm_display_scale) if runtime.current_sweep_norm is not None else None,
|
||||
]
|
||||
y_limits = compute_auto_ylim(*y_series)
|
||||
if y_limits is not None:
|
||||
p_line.setYRange(y_limits[0], y_limits[1], padding=0)
|
||||
if refresh_auto_ranges and y_limits is not None:
|
||||
set_y_range_if_changed("line_y", p_line, y_limits[0], y_limits[1], padding=0)
|
||||
if p_line_aux_vb is not None:
|
||||
aux_limits = compute_auto_ylim(
|
||||
displayed_aux[0] if displayed_aux is not None else None,
|
||||
@ -2754,16 +2891,16 @@ def run_pyqtgraph(args) -> None:
|
||||
displayed_tagged_aux_high[0] if displayed_tagged_aux_high is not None else None,
|
||||
displayed_tagged_aux_high[1] if displayed_tagged_aux_high is not None else None,
|
||||
)
|
||||
if aux_limits is not None:
|
||||
p_line_aux_vb.setYRange(aux_limits[0], aux_limits[1], padding=0)
|
||||
if refresh_auto_ranges and aux_limits is not None:
|
||||
set_y_range_if_changed("line_aux_y", p_line_aux_vb, aux_limits[0], aux_limits[1], padding=0)
|
||||
phase_limits = compute_auto_ylim(displayed_phase, displayed_phase_high)
|
||||
if phase_limits is not None:
|
||||
p_line_phase.setYRange(phase_limits[0], phase_limits[1], padding=0)
|
||||
if refresh_auto_ranges and phase_limits is not None:
|
||||
set_y_range_if_changed("line_phase_y", p_line_phase, phase_limits[0], phase_limits[1], padding=0)
|
||||
|
||||
line_x_bounds = resolve_axis_bounds(xs)
|
||||
if line_x_bounds is not None:
|
||||
p_line.setXRange(line_x_bounds[0], line_x_bounds[1], padding=0)
|
||||
p_line_phase.setXRange(line_x_bounds[0], line_x_bounds[1], padding=0)
|
||||
set_x_range_if_changed("line_x", p_line, line_x_bounds[0], line_x_bounds[1], padding=0)
|
||||
set_x_range_if_changed("line_phase_x", p_line_phase, line_x_bounds[0], line_x_bounds[1], padding=0)
|
||||
|
||||
complex_calib_plot_signal: Optional[np.ndarray] = None
|
||||
if (
|
||||
@ -2785,11 +2922,23 @@ def run_pyqtgraph(args) -> None:
|
||||
curve_complex_calib_real.setData(real_x, real_y, autoDownsample=False)
|
||||
curve_complex_calib_imag.setData(imag_x, imag_y, autoDownsample=False)
|
||||
complex_ylim = compute_auto_ylim(real_after, imag_after)
|
||||
if complex_ylim is not None:
|
||||
p_complex_calib.setYRange(complex_ylim[0], complex_ylim[1], padding=0)
|
||||
if refresh_auto_ranges and complex_ylim is not None:
|
||||
set_y_range_if_changed(
|
||||
"complex_calib_y",
|
||||
p_complex_calib,
|
||||
complex_ylim[0],
|
||||
complex_ylim[1],
|
||||
padding=0,
|
||||
)
|
||||
complex_x_bounds = resolve_axis_bounds(xs_complex)
|
||||
if complex_x_bounds is not None:
|
||||
p_complex_calib.setXRange(complex_x_bounds[0], complex_x_bounds[1], padding=0)
|
||||
set_x_range_if_changed(
|
||||
"complex_calib_x",
|
||||
p_complex_calib,
|
||||
complex_x_bounds[0],
|
||||
complex_x_bounds[1],
|
||||
padding=0,
|
||||
)
|
||||
else:
|
||||
curve_complex_calib_real.setData([], [])
|
||||
curve_complex_calib_imag.setData([], [])
|
||||
@ -2844,7 +2993,7 @@ def run_pyqtgraph(args) -> None:
|
||||
fft_complex_plot = None
|
||||
fft_x_bounds = resolve_axis_bounds(xs_fft)
|
||||
if fft_x_bounds is not None:
|
||||
p_fft.setXRange(fft_x_bounds[0], fft_x_bounds[1], padding=0)
|
||||
set_x_range_if_changed("fft_x", p_fft, fft_x_bounds[0], fft_x_bounds[1], padding=0)
|
||||
|
||||
fft_vals_db = fft_mag_to_db(fft_mag_plot)
|
||||
ref_curve_for_range = None
|
||||
@ -2911,8 +3060,8 @@ def run_pyqtgraph(args) -> None:
|
||||
box.setVisible(False)
|
||||
|
||||
y_limits = compute_auto_ylim(visible_abs, visible_real, visible_imag, ref_curve_for_range)
|
||||
if y_limits is not None:
|
||||
p_fft.setYRange(y_limits[0], y_limits[1], padding=0)
|
||||
if refresh_auto_ranges and y_limits is not None:
|
||||
set_y_range_if_changed("fft_y", p_fft, y_limits[0], y_limits[1], padding=0)
|
||||
|
||||
if peak_calibrate_mode and visible_abs is not None:
|
||||
markers = find_peak_width_markers(xs_fft, fft_vals_db)
|
||||
@ -2986,7 +3135,8 @@ def run_pyqtgraph(args) -> None:
|
||||
box.setVisible(False)
|
||||
|
||||
if active_background is not None and fft_abs_enabled:
|
||||
p_fft.setYRange(-10.0, 30.0, padding=0)
|
||||
if refresh_auto_ranges:
|
||||
set_y_range_if_changed("fft_y", p_fft, -10.0, 30.0, padding=0)
|
||||
else:
|
||||
finite_y = y_for_range[np.isfinite(y_for_range)]
|
||||
if finite_y.size > 0:
|
||||
@ -2994,7 +3144,8 @@ def run_pyqtgraph(args) -> None:
|
||||
y1 = float(np.max(finite_y))
|
||||
if y1 <= y0:
|
||||
y1 = y0 + 1e-3
|
||||
p_fft.setYRange(y0, y1, padding=0)
|
||||
if refresh_auto_ranges:
|
||||
set_y_range_if_changed("fft_y", p_fft, y0, y1, padding=0)
|
||||
|
||||
if peak_calibrate_mode and fft_abs_enabled:
|
||||
markers = find_peak_width_markers(xs_fft, fft_vals_db)
|
||||
@ -3202,8 +3353,14 @@ def run_pyqtgraph(args) -> None:
|
||||
distance_bounds = resolve_axis_bounds(disp_fft_axis)
|
||||
if distance_bounds is not None:
|
||||
d_min, d_max = distance_bounds
|
||||
set_image_rect_if_ready(img_fft, 0.0, d_min, float(max_sweeps), max(1e-9, d_max - d_min))
|
||||
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(d_min, d_max), padding=0)
|
||||
set_image_rect_if_changed("fft_waterfall_rect", img_fft, 0.0, d_min, float(max_sweeps), max(1e-9, d_max - d_min))
|
||||
set_xy_range_if_changed(
|
||||
"fft_waterfall_range",
|
||||
p_spec,
|
||||
x_bounds=(0, max_sweeps - 1),
|
||||
y_bounds=(d_min, d_max),
|
||||
padding=0,
|
||||
)
|
||||
if levels is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user