done
This commit is contained in:
@ -12,7 +12,7 @@ from rfg_adc_plotter.constants import FFT_LEN, FREQ_SPAN_GHZ, IFFT_LEN
|
||||
_IFFT_T_MAX_NS = float((IFFT_LEN - 1) / (FREQ_SPAN_GHZ * 1e9) * 1e9)
|
||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||
from rfg_adc_plotter.processing.normalizer import build_calib_envelopes
|
||||
from rfg_adc_plotter.state.app_state import BACKGROUND_PATH, CALIB_ENVELOPE_PATH, AppState, format_status
|
||||
from rfg_adc_plotter.state.app_state import AppState, format_status
|
||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||
from rfg_adc_plotter.types import SweepPacket
|
||||
|
||||
@ -82,7 +82,8 @@ def run_matplotlib(args):
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.animation import FuncAnimation
|
||||
from matplotlib.widgets import CheckButtons, Slider
|
||||
from matplotlib.widgets import Button as MplButton
|
||||
from matplotlib.widgets import CheckButtons, RadioButtons, Slider, TextBox
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[error] Нужны matplotlib и её зависимости: {e}\n")
|
||||
sys.exit(1)
|
||||
@ -111,6 +112,7 @@ def run_matplotlib(args):
|
||||
logscale_enabled = bool(getattr(args, "logscale", False))
|
||||
|
||||
state = AppState(norm_type=norm_type)
|
||||
state.configure_capture_import(fancy=bool(args.fancy), logscale=bool(getattr(args, "logscale", False)))
|
||||
ring = RingBuffer(max_sweeps)
|
||||
|
||||
# --- Создание фигуры ---
|
||||
@ -118,10 +120,12 @@ def run_matplotlib(args):
|
||||
(ax_line, ax_img), (ax_fft, ax_spec) = axs
|
||||
if hasattr(fig.canvas.manager, "set_window_title"):
|
||||
fig.canvas.manager.set_window_title(args.title)
|
||||
fig.subplots_adjust(wspace=0.25, hspace=0.35, left=0.07, right=0.90, top=0.92, bottom=0.08)
|
||||
fig.subplots_adjust(wspace=0.25, hspace=0.35, left=0.07, right=0.90, top=0.92, bottom=0.22)
|
||||
|
||||
# Статусная строка
|
||||
status_text = fig.text(0.01, 0.01, "", ha="left", va="bottom", fontsize=8, family="monospace")
|
||||
pipeline_text = fig.text(0.01, 0.03, "", ha="left", va="bottom", fontsize=8, family="monospace")
|
||||
ref_text = fig.text(0.01, 0.05, "", ha="left", va="bottom", fontsize=8, family="monospace")
|
||||
|
||||
# График последнего свипа
|
||||
line_obj, = ax_line.plot([], [], lw=1, color="tab:blue")
|
||||
@ -178,15 +182,144 @@ def run_matplotlib(args):
|
||||
ax_sctr = fig.add_axes([0.98, 0.55, 0.02, 0.35])
|
||||
ax_cb = fig.add_axes([0.92, 0.45, 0.08, 0.08])
|
||||
ax_cb_file = fig.add_axes([0.92, 0.36, 0.08, 0.08])
|
||||
ax_line_mode = fig.add_axes([0.92, 0.10, 0.08, 0.08])
|
||||
ymin_slider = Slider(ax_smin, "Y min", 0, max(1, fft_bins - 1), valinit=0, valstep=1, orientation="vertical")
|
||||
ymax_slider = Slider(ax_smax, "Y max", 0, max(1, fft_bins - 1), valinit=max(1, fft_bins - 1), valstep=1, orientation="vertical")
|
||||
contrast_slider = Slider(ax_sctr, "Int max", 0, 100, valinit=100, valstep=1, orientation="vertical")
|
||||
calib_cb = CheckButtons(ax_cb, ["калибровка"], [False])
|
||||
calib_file_cb = CheckButtons(ax_cb_file, ["из файла"], [False])
|
||||
line_mode_rb = RadioButtons(ax_line_mode, ("raw", "processed"), active=0)
|
||||
try:
|
||||
ax_line_mode.set_title("Линия", fontsize=8, pad=2)
|
||||
except Exception:
|
||||
pass
|
||||
line_mode_state = {"value": "raw"}
|
||||
|
||||
import os as _os
|
||||
if not _os.path.isfile(CALIB_ENVELOPE_PATH):
|
||||
ax_cb_file.set_visible(False)
|
||||
try:
|
||||
import tkinter as _tk
|
||||
from tkinter import filedialog as _tk_filedialog
|
||||
_tk_available = True
|
||||
except Exception:
|
||||
_tk = None
|
||||
_tk_filedialog = None
|
||||
_tk_available = False
|
||||
|
||||
# Нижняя панель путей и кнопок (работает без Qt; выбор файла через tkinter опционален).
|
||||
ax_calib_path = fig.add_axes([0.07, 0.14, 0.40, 0.04])
|
||||
ax_calib_load = fig.add_axes([0.48, 0.14, 0.07, 0.04])
|
||||
ax_calib_pick = fig.add_axes([0.56, 0.14, 0.06, 0.04])
|
||||
ax_calib_sample = fig.add_axes([0.63, 0.14, 0.09, 0.04])
|
||||
ax_calib_save = fig.add_axes([0.73, 0.14, 0.10, 0.04])
|
||||
|
||||
ax_bg_path = fig.add_axes([0.07, 0.09, 0.40, 0.04])
|
||||
ax_bg_load = fig.add_axes([0.48, 0.09, 0.07, 0.04])
|
||||
ax_bg_pick = fig.add_axes([0.56, 0.09, 0.06, 0.04])
|
||||
ax_bg_sample = fig.add_axes([0.63, 0.09, 0.09, 0.04])
|
||||
ax_bg_save2 = fig.add_axes([0.73, 0.09, 0.10, 0.04])
|
||||
|
||||
calib_path_box = TextBox(ax_calib_path, "Калибр", initial=state.calib_envelope_path)
|
||||
bg_path_box = TextBox(ax_bg_path, "Фон", initial=state.background_path)
|
||||
|
||||
calib_load_btn2 = MplButton(ax_calib_load, "Загруз.")
|
||||
calib_pick_btn2 = MplButton(ax_calib_pick, "Файл")
|
||||
calib_sample_btn2 = MplButton(ax_calib_sample, "sample")
|
||||
calib_save_btn2 = MplButton(ax_calib_save, "Сохр env")
|
||||
|
||||
bg_load_btn2 = MplButton(ax_bg_load, "Загруз.")
|
||||
bg_pick_btn2 = MplButton(ax_bg_pick, "Файл")
|
||||
bg_sample_btn2 = MplButton(ax_bg_sample, "sample")
|
||||
bg_save_btn2 = MplButton(ax_bg_save2, "Сохр фон")
|
||||
|
||||
if not _tk_available:
|
||||
try:
|
||||
calib_pick_btn2.label.set_text("Файл-")
|
||||
bg_pick_btn2.label.set_text("Файл-")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _tb_text(tb):
|
||||
try:
|
||||
return str(tb.text).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _pick_file_dialog(initial_path: str) -> str:
|
||||
if not _tk_available or _tk is None or _tk_filedialog is None:
|
||||
return ""
|
||||
root = None
|
||||
try:
|
||||
root = _tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes("-topmost", True)
|
||||
except Exception:
|
||||
root = None
|
||||
try:
|
||||
return str(
|
||||
_tk_filedialog.askopenfilename(
|
||||
initialdir=_os.path.dirname(initial_path) or ".",
|
||||
initialfile=_os.path.basename(initial_path) or "",
|
||||
title="Выбрать файл эталона (.npy или capture)",
|
||||
)
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
if root is not None:
|
||||
root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _sync_path_boxes():
|
||||
try:
|
||||
if _tb_text(calib_path_box) != state.calib_envelope_path:
|
||||
calib_path_box.set_val(state.calib_envelope_path)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if _tb_text(bg_path_box) != state.background_path:
|
||||
bg_path_box.set_val(state.background_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _refresh_status_texts():
|
||||
pipeline_text.set_text(state.format_pipeline_status())
|
||||
ref_text.set_text(state.format_reference_status())
|
||||
try:
|
||||
fig.canvas.draw_idle()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _line_mode() -> str:
|
||||
return str(line_mode_state.get("value", "raw"))
|
||||
|
||||
def _refresh_checkboxes():
|
||||
try:
|
||||
# file-mode чекбокс показываем всегда; он активен при наличии пути/данных.
|
||||
ax_cb_file.set_visible(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _load_calib_from_ui():
|
||||
p = _tb_text(calib_path_box)
|
||||
if p:
|
||||
state.set_calib_envelope_path(p)
|
||||
ok = state.load_calib_reference()
|
||||
if ok and bool(calib_file_cb.get_status()[0]):
|
||||
state.set_calib_mode("file")
|
||||
state.set_calib_enabled(bool(calib_cb.get_status()[0]))
|
||||
_sync_path_boxes()
|
||||
_refresh_checkboxes()
|
||||
_refresh_status_texts()
|
||||
return ok
|
||||
|
||||
def _load_bg_from_ui():
|
||||
p = _tb_text(bg_path_box)
|
||||
if p:
|
||||
state.set_background_path(p)
|
||||
ok = state.load_background_reference()
|
||||
_sync_path_boxes()
|
||||
_refresh_status_texts()
|
||||
return ok
|
||||
|
||||
def _on_ylim_change(_val):
|
||||
try:
|
||||
@ -200,7 +333,7 @@ def run_matplotlib(args):
|
||||
def _on_calib_file_clicked(_v):
|
||||
use_file = bool(calib_file_cb.get_status()[0])
|
||||
if use_file:
|
||||
ok = state.load_calib_envelope(CALIB_ENVELOPE_PATH)
|
||||
ok = _load_calib_from_ui()
|
||||
if ok:
|
||||
state.set_calib_mode("file")
|
||||
else:
|
||||
@ -208,17 +341,15 @@ def run_matplotlib(args):
|
||||
else:
|
||||
state.set_calib_mode("live")
|
||||
state.set_calib_enabled(bool(calib_cb.get_status()[0]))
|
||||
_refresh_status_texts()
|
||||
|
||||
def _on_calib_clicked(_v):
|
||||
import os as _os2
|
||||
if _os2.path.isfile(CALIB_ENVELOPE_PATH):
|
||||
ax_cb_file.set_visible(True)
|
||||
state.set_calib_enabled(bool(calib_cb.get_status()[0]))
|
||||
fig.canvas.draw_idle()
|
||||
_refresh_checkboxes()
|
||||
_refresh_status_texts()
|
||||
|
||||
ax_btn_bg = fig.add_axes([0.92, 0.27, 0.08, 0.05])
|
||||
ax_cb_bg = fig.add_axes([0.92, 0.20, 0.08, 0.06])
|
||||
from matplotlib.widgets import Button as MplButton
|
||||
save_bg_btn = MplButton(ax_btn_bg, "Сохр. фон")
|
||||
bg_cb = CheckButtons(ax_cb_bg, ["вычет фона"], [False])
|
||||
|
||||
@ -226,21 +357,88 @@ def run_matplotlib(args):
|
||||
ok = state.save_background()
|
||||
if ok:
|
||||
state.load_background()
|
||||
fig.canvas.draw_idle()
|
||||
_sync_path_boxes()
|
||||
_refresh_status_texts()
|
||||
|
||||
def _on_bg_clicked(_v):
|
||||
state.set_background_enabled(bool(bg_cb.get_status()[0]))
|
||||
_refresh_status_texts()
|
||||
|
||||
def _on_calib_load_btn(_event):
|
||||
_load_calib_from_ui()
|
||||
|
||||
def _on_calib_pick_btn(_event):
|
||||
path = _pick_file_dialog(_tb_text(calib_path_box) or state.calib_envelope_path)
|
||||
if not path:
|
||||
return
|
||||
state.set_calib_envelope_path(path)
|
||||
_sync_path_boxes()
|
||||
_refresh_status_texts()
|
||||
|
||||
def _on_calib_sample_btn(_event):
|
||||
state.set_calib_envelope_path(_os.path.join("sample_data", "no_antennas_35dB_attenuators"))
|
||||
_sync_path_boxes()
|
||||
if _load_calib_from_ui() and not bool(calib_file_cb.get_status()[0]):
|
||||
calib_file_cb.set_active(0)
|
||||
|
||||
def _on_calib_save_btn(_event):
|
||||
state.save_calib_envelope()
|
||||
_sync_path_boxes()
|
||||
_refresh_status_texts()
|
||||
|
||||
def _on_bg_load_btn(_event):
|
||||
_load_bg_from_ui()
|
||||
|
||||
def _on_bg_pick_btn(_event):
|
||||
path = _pick_file_dialog(_tb_text(bg_path_box) or state.background_path)
|
||||
if not path:
|
||||
return
|
||||
state.set_background_path(path)
|
||||
_sync_path_boxes()
|
||||
_refresh_status_texts()
|
||||
|
||||
def _on_bg_sample_btn(_event):
|
||||
state.set_background_path(_os.path.join("sample_data", "empty"))
|
||||
_sync_path_boxes()
|
||||
_load_bg_from_ui()
|
||||
|
||||
def _on_bg_save_btn2(_event):
|
||||
ok = state.save_background()
|
||||
if ok:
|
||||
state.load_background()
|
||||
_sync_path_boxes()
|
||||
_refresh_status_texts()
|
||||
|
||||
def _on_line_mode_clicked(label):
|
||||
line_mode_state["value"] = str(label)
|
||||
try:
|
||||
fig.canvas.draw_idle()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
save_bg_btn.on_clicked(_on_save_bg)
|
||||
bg_cb.on_clicked(_on_bg_clicked)
|
||||
calib_load_btn2.on_clicked(_on_calib_load_btn)
|
||||
calib_pick_btn2.on_clicked(_on_calib_pick_btn)
|
||||
calib_sample_btn2.on_clicked(_on_calib_sample_btn)
|
||||
calib_save_btn2.on_clicked(_on_calib_save_btn)
|
||||
bg_load_btn2.on_clicked(_on_bg_load_btn)
|
||||
bg_pick_btn2.on_clicked(_on_bg_pick_btn)
|
||||
bg_sample_btn2.on_clicked(_on_bg_sample_btn)
|
||||
bg_save_btn2.on_clicked(_on_bg_save_btn2)
|
||||
line_mode_rb.on_clicked(_on_line_mode_clicked)
|
||||
|
||||
ymin_slider.on_changed(_on_ylim_change)
|
||||
ymax_slider.on_changed(_on_ylim_change)
|
||||
contrast_slider.on_changed(lambda _v: fig.canvas.draw_idle())
|
||||
calib_cb.on_clicked(_on_calib_clicked)
|
||||
calib_file_cb.on_clicked(_on_calib_file_clicked)
|
||||
_sync_path_boxes()
|
||||
_refresh_checkboxes()
|
||||
_refresh_status_texts()
|
||||
except Exception:
|
||||
calib_cb = None
|
||||
line_mode_state = {"value": "raw"}
|
||||
|
||||
FREQ_MIN = 3.323
|
||||
FREQ_MAX = 14.323
|
||||
@ -276,23 +474,37 @@ def run_matplotlib(args):
|
||||
xs = ring.x_shared[: raw.size]
|
||||
else:
|
||||
xs = np.arange(raw.size, dtype=np.int32)
|
||||
line_obj.set_data(xs, raw)
|
||||
if state.calib_mode == "file" and state.calib_file_envelope is not None:
|
||||
upper = state.calib_file_envelope
|
||||
lower = -upper
|
||||
m_env = float(np.nanmax(np.abs(upper)))
|
||||
if m_env <= 0.0:
|
||||
m_env = 1.0
|
||||
line_env_lo.set_data(xs[: upper.size], lower / m_env)
|
||||
line_env_hi.set_data(xs[: upper.size], upper / m_env)
|
||||
elif state.last_calib_sweep is not None:
|
||||
calib = state.last_calib_sweep
|
||||
m_calib = float(np.nanmax(np.abs(calib)))
|
||||
if m_calib <= 0.0:
|
||||
m_calib = 1.0
|
||||
lower, upper = build_calib_envelopes(calib)
|
||||
line_env_lo.set_data(xs[: calib.size], lower / m_calib)
|
||||
line_env_hi.set_data(xs[: calib.size], upper / m_calib)
|
||||
line_mode = str(line_mode_state.get("value", "raw"))
|
||||
main = state.current_sweep_processed if line_mode == "processed" else raw
|
||||
if main is not None:
|
||||
line_obj.set_data(xs[: main.size], main)
|
||||
else:
|
||||
line_obj.set_data([], [])
|
||||
if line_mode == "raw":
|
||||
if state.calib_mode == "file" and state.calib_file_envelope is not None:
|
||||
upper = np.asarray(state.calib_file_envelope, dtype=np.float32)
|
||||
n_env = min(xs.size, upper.size)
|
||||
if n_env > 0:
|
||||
x_env = xs[:n_env]
|
||||
y_env = upper[:n_env]
|
||||
line_env_lo.set_data(x_env, -y_env)
|
||||
line_env_hi.set_data(x_env, y_env)
|
||||
else:
|
||||
line_env_lo.set_data([], [])
|
||||
line_env_hi.set_data([], [])
|
||||
elif state.last_calib_sweep is not None:
|
||||
calib = np.asarray(state.last_calib_sweep, dtype=np.float32)
|
||||
lower, upper = build_calib_envelopes(calib)
|
||||
n_env = min(xs.size, lower.size, upper.size)
|
||||
if n_env > 0:
|
||||
line_env_lo.set_data(xs[:n_env], lower[:n_env])
|
||||
line_env_hi.set_data(xs[:n_env], upper[:n_env])
|
||||
else:
|
||||
line_env_lo.set_data([], [])
|
||||
line_env_hi.set_data([], [])
|
||||
else:
|
||||
line_env_lo.set_data([], [])
|
||||
line_env_hi.set_data([], [])
|
||||
else:
|
||||
line_env_lo.set_data([], [])
|
||||
line_env_hi.set_data([], [])
|
||||
@ -306,16 +518,19 @@ def run_matplotlib(args):
|
||||
post = state.current_sweep_post_exp if state.current_sweep_post_exp is not None else raw
|
||||
line_post_exp_obj.set_data(xs[: post.size], post)
|
||||
|
||||
if state.current_sweep_processed is not None:
|
||||
proc = state.current_sweep_processed
|
||||
line_obj.set_data(xs[: proc.size], proc)
|
||||
if line_mode == "processed":
|
||||
if state.current_sweep_processed is not None:
|
||||
proc = state.current_sweep_processed
|
||||
line_obj.set_data(xs[: proc.size], proc)
|
||||
else:
|
||||
line_obj.set_data([], [])
|
||||
else:
|
||||
line_obj.set_data([], [])
|
||||
line_obj.set_data(xs[: raw.size], raw)
|
||||
line_norm_obj.set_data([], [])
|
||||
else:
|
||||
line_pre_exp_obj.set_data([], [])
|
||||
line_post_exp_obj.set_data([], [])
|
||||
if state.current_sweep_norm is not None:
|
||||
if line_mode == "raw" and state.current_sweep_norm is not None:
|
||||
line_norm_obj.set_data(
|
||||
xs[: state.current_sweep_norm.size], state.current_sweep_norm
|
||||
)
|
||||
@ -370,6 +585,11 @@ def run_matplotlib(args):
|
||||
if changed and state.current_info:
|
||||
status_text.set_text(format_status(state.current_info))
|
||||
channel_text.set_text(state.format_channel_label())
|
||||
pipeline_text.set_text(state.format_pipeline_status())
|
||||
ref_text.set_text(state.format_reference_status())
|
||||
elif changed:
|
||||
pipeline_text.set_text(state.format_pipeline_status())
|
||||
ref_text.set_text(state.format_reference_status())
|
||||
|
||||
return (
|
||||
line_obj,
|
||||
@ -382,6 +602,8 @@ def run_matplotlib(args):
|
||||
fft_line_obj,
|
||||
img_fft_obj,
|
||||
status_text,
|
||||
pipeline_text,
|
||||
ref_text,
|
||||
channel_text,
|
||||
)
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""PyQtGraph-бэкенд реалтайм-плоттера свипов."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from queue import Queue
|
||||
@ -10,7 +11,7 @@ import numpy as np
|
||||
from rfg_adc_plotter.constants import FREQ_SPAN_GHZ, IFFT_LEN
|
||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||
from rfg_adc_plotter.processing.normalizer import build_calib_envelopes
|
||||
from rfg_adc_plotter.state.app_state import BACKGROUND_PATH, CALIB_ENVELOPE_PATH, AppState, format_status
|
||||
from rfg_adc_plotter.state.app_state import AppState, format_status
|
||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||
from rfg_adc_plotter.types import SweepPacket
|
||||
|
||||
@ -90,6 +91,18 @@ def _visible_levels(
|
||||
return (vmin, vmax)
|
||||
|
||||
|
||||
def _short_path(path: str, max_len: int = 48) -> str:
|
||||
p = str(path or "").strip()
|
||||
if not p:
|
||||
return "(не задан)"
|
||||
if len(p) <= max_len:
|
||||
return p
|
||||
base = os.path.basename(p)
|
||||
if len(base) <= max_len:
|
||||
return f".../{base}"
|
||||
return "..." + p[-(max_len - 3) :]
|
||||
|
||||
|
||||
def run_pyqtgraph(args):
|
||||
"""Быстрый GUI на PyQtGraph. Требует pyqtgraph и PyQt5/PySide6."""
|
||||
try:
|
||||
@ -128,13 +141,22 @@ def run_pyqtgraph(args):
|
||||
logscale_enabled = bool(getattr(args, "logscale", False))
|
||||
|
||||
state = AppState(norm_type=norm_type)
|
||||
state.configure_capture_import(fancy=bool(args.fancy), logscale=bool(getattr(args, "logscale", False)))
|
||||
ring = RingBuffer(max_sweeps)
|
||||
|
||||
try:
|
||||
_qt_text_selectable = QtCore.Qt.TextSelectableByMouse
|
||||
except Exception:
|
||||
try:
|
||||
_qt_text_selectable = QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
except Exception:
|
||||
_qt_text_selectable = None
|
||||
|
||||
# --- Создание окна ---
|
||||
pg.setConfigOptions(useOpenGL=True, antialias=False)
|
||||
app = pg.mkQApp(args.title)
|
||||
win = pg.GraphicsLayoutWidget(show=True, title=args.title)
|
||||
win.resize(1200, 600)
|
||||
win.resize(1280, 760)
|
||||
|
||||
# График последнего свипа (слева-сверху)
|
||||
p_line = win.addPlot(row=0, col=0, title="Сырые данные")
|
||||
@ -196,32 +218,58 @@ def run_pyqtgraph(args):
|
||||
img_fft = pg.ImageItem()
|
||||
p_spec.addItem(img_fft)
|
||||
|
||||
# Чекбоксы калибровки — в одном контейнере
|
||||
# Блок управления калибровкой
|
||||
calib_widget = QtWidgets.QWidget()
|
||||
calib_layout = QtWidgets.QHBoxLayout(calib_widget)
|
||||
calib_layout = QtWidgets.QVBoxLayout(calib_widget)
|
||||
calib_layout.setContentsMargins(2, 2, 2, 2)
|
||||
calib_layout.setSpacing(8)
|
||||
calib_layout.setSpacing(4)
|
||||
|
||||
calib_row_1 = QtWidgets.QHBoxLayout()
|
||||
calib_row_1.setSpacing(8)
|
||||
calib_row_2 = QtWidgets.QHBoxLayout()
|
||||
calib_row_2.setSpacing(6)
|
||||
|
||||
calib_cb = QtWidgets.QCheckBox("калибровка")
|
||||
calib_file_cb = QtWidgets.QCheckBox("из файла")
|
||||
calib_file_cb.setEnabled(False) # активируется только если файл существует
|
||||
calib_file_cb.setEnabled(False)
|
||||
calib_path_label = QtWidgets.QLabel()
|
||||
calib_path_label.setMinimumWidth(260)
|
||||
if _qt_text_selectable is not None:
|
||||
calib_path_label.setTextInteractionFlags(_qt_text_selectable)
|
||||
calib_pick_btn = QtWidgets.QPushButton("Файл…")
|
||||
calib_load_btn = QtWidgets.QPushButton("Загрузить")
|
||||
calib_save_btn = QtWidgets.QPushButton("Сохранить env")
|
||||
calib_sample_btn = QtWidgets.QPushButton("sample calib")
|
||||
|
||||
calib_layout.addWidget(calib_cb)
|
||||
calib_layout.addWidget(calib_file_cb)
|
||||
calib_row_1.addWidget(calib_cb)
|
||||
calib_row_1.addWidget(calib_file_cb)
|
||||
calib_row_1.addStretch(1)
|
||||
|
||||
calib_row_2.addWidget(QtWidgets.QLabel("Калибр:"))
|
||||
calib_row_2.addWidget(calib_path_label, 1)
|
||||
calib_row_2.addWidget(calib_pick_btn)
|
||||
calib_row_2.addWidget(calib_load_btn)
|
||||
calib_row_2.addWidget(calib_save_btn)
|
||||
calib_row_2.addWidget(calib_sample_btn)
|
||||
|
||||
calib_layout.addLayout(calib_row_1)
|
||||
calib_layout.addLayout(calib_row_2)
|
||||
|
||||
cb_container_proxy = QtWidgets.QGraphicsProxyWidget()
|
||||
cb_container_proxy.setWidget(calib_widget)
|
||||
win.addItem(cb_container_proxy, row=2, col=1)
|
||||
|
||||
def _check_file_cb_available():
|
||||
import os
|
||||
calib_file_cb.setEnabled(os.path.isfile(CALIB_ENVELOPE_PATH))
|
||||
|
||||
_check_file_cb_available()
|
||||
def _refresh_calib_controls():
|
||||
calib_path_label.setText(_short_path(state.calib_envelope_path))
|
||||
calib_path_label.setToolTip(state.calib_envelope_path)
|
||||
calib_load_btn.setEnabled(bool(state.calib_envelope_path) and os.path.isfile(state.calib_envelope_path))
|
||||
calib_save_btn.setEnabled(state.last_calib_sweep is not None)
|
||||
# Переключатель file-mode доступен, если файл существует или уже загружен в память.
|
||||
calib_file_cb.setEnabled(state.has_calib_envelope_file() or state.calib_file_envelope is not None)
|
||||
|
||||
def _on_calib_file_toggled(checked):
|
||||
if checked:
|
||||
ok = state.load_calib_envelope(CALIB_ENVELOPE_PATH)
|
||||
ok = state.load_calib_reference()
|
||||
if ok:
|
||||
state.set_calib_mode("file")
|
||||
else:
|
||||
@ -229,43 +277,196 @@ def run_pyqtgraph(args):
|
||||
else:
|
||||
state.set_calib_mode("live")
|
||||
state.set_calib_enabled(calib_cb.isChecked())
|
||||
_refresh_calib_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_calib_toggled(_v):
|
||||
_check_file_cb_available()
|
||||
state.set_calib_enabled(calib_cb.isChecked())
|
||||
_refresh_calib_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_pick_calib_path():
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
win,
|
||||
"Выбрать источник калибровки (.npy или capture)",
|
||||
state.calib_envelope_path,
|
||||
"Все файлы (*);;NumPy (*.npy)",
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
state.set_calib_envelope_path(path)
|
||||
if calib_file_cb.isChecked():
|
||||
if state.load_calib_reference():
|
||||
state.set_calib_mode("file")
|
||||
state.set_calib_enabled(calib_cb.isChecked())
|
||||
else:
|
||||
calib_file_cb.setChecked(False)
|
||||
_refresh_calib_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_load_calib():
|
||||
if state.load_calib_reference():
|
||||
if calib_file_cb.isChecked():
|
||||
state.set_calib_mode("file")
|
||||
state.set_calib_enabled(calib_cb.isChecked())
|
||||
_refresh_calib_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_save_calib():
|
||||
if state.save_calib_envelope():
|
||||
if calib_file_cb.isChecked():
|
||||
state.load_calib_envelope()
|
||||
state.set_calib_mode("file")
|
||||
state.set_calib_enabled(calib_cb.isChecked())
|
||||
_refresh_calib_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_sample_calib():
|
||||
sample_path = os.path.join("sample_data", "no_antennas_35dB_attenuators")
|
||||
state.set_calib_envelope_path(sample_path)
|
||||
if state.load_calib_reference():
|
||||
calib_file_cb.setChecked(True)
|
||||
state.set_calib_mode("file")
|
||||
state.set_calib_enabled(calib_cb.isChecked())
|
||||
_refresh_calib_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
calib_cb.stateChanged.connect(_on_calib_toggled)
|
||||
calib_file_cb.stateChanged.connect(lambda _v: _on_calib_file_toggled(calib_file_cb.isChecked()))
|
||||
calib_pick_btn.clicked.connect(_on_pick_calib_path)
|
||||
calib_load_btn.clicked.connect(_on_load_calib)
|
||||
calib_save_btn.clicked.connect(_on_save_calib)
|
||||
calib_sample_btn.clicked.connect(_on_sample_calib)
|
||||
|
||||
# Кнопка сохранения фона + чекбокс вычета фона
|
||||
# Блок управления фоном
|
||||
bg_widget = QtWidgets.QWidget()
|
||||
bg_layout = QtWidgets.QHBoxLayout(bg_widget)
|
||||
bg_layout = QtWidgets.QVBoxLayout(bg_widget)
|
||||
bg_layout.setContentsMargins(2, 2, 2, 2)
|
||||
bg_layout.setSpacing(8)
|
||||
bg_layout.setSpacing(4)
|
||||
|
||||
save_bg_btn = QtWidgets.QPushButton("Сохр. фон")
|
||||
bg_row_1 = QtWidgets.QHBoxLayout()
|
||||
bg_row_1.setSpacing(8)
|
||||
bg_row_2 = QtWidgets.QHBoxLayout()
|
||||
bg_row_2.setSpacing(6)
|
||||
|
||||
save_bg_btn = QtWidgets.QPushButton("Сохранить фон")
|
||||
load_bg_btn = QtWidgets.QPushButton("Загрузить")
|
||||
bg_pick_btn = QtWidgets.QPushButton("Файл…")
|
||||
bg_sample_btn = QtWidgets.QPushButton("sample bg")
|
||||
bg_cb = QtWidgets.QCheckBox("вычет фона")
|
||||
bg_cb.setEnabled(False)
|
||||
bg_cb.setEnabled(False) # активируется при успешной загрузке/сохранении
|
||||
bg_path_label = QtWidgets.QLabel()
|
||||
bg_path_label.setMinimumWidth(260)
|
||||
if _qt_text_selectable is not None:
|
||||
bg_path_label.setTextInteractionFlags(_qt_text_selectable)
|
||||
|
||||
bg_layout.addWidget(save_bg_btn)
|
||||
bg_layout.addWidget(bg_cb)
|
||||
bg_row_1.addWidget(bg_cb)
|
||||
bg_row_1.addStretch(1)
|
||||
|
||||
bg_row_2.addWidget(QtWidgets.QLabel("Фон:"))
|
||||
bg_row_2.addWidget(bg_path_label, 1)
|
||||
bg_row_2.addWidget(bg_pick_btn)
|
||||
bg_row_2.addWidget(load_bg_btn)
|
||||
bg_row_2.addWidget(save_bg_btn)
|
||||
bg_row_2.addWidget(bg_sample_btn)
|
||||
|
||||
bg_layout.addLayout(bg_row_1)
|
||||
bg_layout.addLayout(bg_row_2)
|
||||
|
||||
bg_container_proxy = QtWidgets.QGraphicsProxyWidget()
|
||||
bg_container_proxy.setWidget(bg_widget)
|
||||
win.addItem(bg_container_proxy, row=2, col=0)
|
||||
|
||||
def _refresh_bg_controls():
|
||||
bg_path_label.setText(_short_path(state.background_path))
|
||||
bg_path_label.setToolTip(state.background_path)
|
||||
load_bg_btn.setEnabled(bool(state.background_path) and os.path.isfile(state.background_path))
|
||||
bg_cb.setEnabled(state.background is not None or state.background_source_type == "capture_raw")
|
||||
|
||||
def _on_pick_bg_path():
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
win,
|
||||
"Выбрать источник фона (.npy или capture)",
|
||||
state.background_path,
|
||||
"Все файлы (*);;NumPy (*.npy)",
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
state.set_background_path(path)
|
||||
if bg_cb.isChecked():
|
||||
if not state.load_background_reference():
|
||||
bg_cb.setChecked(False)
|
||||
_refresh_bg_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_load_bg():
|
||||
state.load_background_reference()
|
||||
_refresh_bg_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_save_bg():
|
||||
ok = state.save_background()
|
||||
if ok:
|
||||
state.load_background()
|
||||
bg_cb.setEnabled(True)
|
||||
_refresh_bg_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_bg_toggled(_v):
|
||||
state.set_background_enabled(bg_cb.isChecked())
|
||||
_refresh_pipeline_label()
|
||||
|
||||
def _on_sample_bg():
|
||||
sample_path = os.path.join("sample_data", "empty")
|
||||
state.set_background_path(sample_path)
|
||||
if state.load_background_reference():
|
||||
bg_cb.setEnabled(True)
|
||||
_refresh_bg_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
bg_pick_btn.clicked.connect(_on_pick_bg_path)
|
||||
load_bg_btn.clicked.connect(_on_load_bg)
|
||||
save_bg_btn.clicked.connect(_on_save_bg)
|
||||
bg_cb.stateChanged.connect(lambda _v: state.set_background_enabled(bg_cb.isChecked()))
|
||||
bg_cb.stateChanged.connect(_on_bg_toggled)
|
||||
bg_sample_btn.clicked.connect(_on_sample_bg)
|
||||
|
||||
# Переключатель отображения верхнего линейного графика
|
||||
line_mode_widget = QtWidgets.QWidget()
|
||||
line_mode_layout = QtWidgets.QHBoxLayout(line_mode_widget)
|
||||
line_mode_layout.setContentsMargins(2, 2, 2, 2)
|
||||
line_mode_layout.setSpacing(8)
|
||||
line_mode_layout.addWidget(QtWidgets.QLabel("Линия:"))
|
||||
line_mode_raw_rb = QtWidgets.QRadioButton("raw")
|
||||
line_mode_proc_rb = QtWidgets.QRadioButton("processed")
|
||||
line_mode_raw_rb.setChecked(True)
|
||||
line_mode_layout.addWidget(line_mode_raw_rb)
|
||||
line_mode_layout.addWidget(line_mode_proc_rb)
|
||||
line_mode_layout.addStretch(1)
|
||||
line_mode_proxy = QtWidgets.QGraphicsProxyWidget()
|
||||
line_mode_proxy.setWidget(line_mode_widget)
|
||||
win.addItem(line_mode_proxy, row=6, col=0, colspan=2)
|
||||
|
||||
def _line_mode() -> str:
|
||||
return "processed" if line_mode_proc_rb.isChecked() else "raw"
|
||||
|
||||
# Статусная строка
|
||||
status = pg.LabelItem(justify="left")
|
||||
win.addItem(status, row=3, col=0, colspan=2)
|
||||
pipeline_status = pg.LabelItem(justify="left")
|
||||
win.addItem(pipeline_status, row=4, col=0, colspan=2)
|
||||
ref_status = pg.LabelItem(justify="left")
|
||||
win.addItem(ref_status, row=5, col=0, colspan=2)
|
||||
|
||||
def _refresh_pipeline_label():
|
||||
txt = state.format_pipeline_status()
|
||||
trace = state.format_stage_trace()
|
||||
if trace:
|
||||
txt = f"{txt} | trace: {trace}"
|
||||
pipeline_status.setText(txt)
|
||||
ref_status.setText(state.format_reference_status())
|
||||
|
||||
_refresh_calib_controls()
|
||||
_refresh_bg_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
_imshow_initialized = [False]
|
||||
|
||||
@ -293,28 +494,46 @@ def run_pyqtgraph(args):
|
||||
if changed and not _imshow_initialized[0] and ring.is_ready:
|
||||
_init_imshow_extents()
|
||||
_imshow_initialized[0] = True
|
||||
if changed:
|
||||
_refresh_calib_controls()
|
||||
_refresh_bg_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
# Линейный график свипа
|
||||
if state.current_sweep_raw is not None and ring.x_shared is not None:
|
||||
raw = state.current_sweep_raw
|
||||
xs = ring.x_shared[: raw.size] if raw.size <= ring.x_shared.size else np.arange(raw.size)
|
||||
curve.setData(xs, raw, autoDownsample=True)
|
||||
if state.calib_mode == "file" and state.calib_file_envelope is not None:
|
||||
upper = state.calib_file_envelope
|
||||
lower = -upper
|
||||
m_env = float(np.nanmax(np.abs(upper)))
|
||||
if m_env <= 0.0:
|
||||
m_env = 1.0
|
||||
curve_env_lo.setData(xs[: upper.size], lower / m_env, autoDownsample=True)
|
||||
curve_env_hi.setData(xs[: upper.size], upper / m_env, autoDownsample=True)
|
||||
elif state.last_calib_sweep is not None:
|
||||
calib = state.last_calib_sweep
|
||||
m_calib = float(np.nanmax(np.abs(calib)))
|
||||
if m_calib <= 0.0:
|
||||
m_calib = 1.0
|
||||
lower, upper = build_calib_envelopes(calib)
|
||||
curve_env_lo.setData(xs[: calib.size], lower / m_calib, autoDownsample=True)
|
||||
curve_env_hi.setData(xs[: calib.size], upper / m_calib, autoDownsample=True)
|
||||
line_mode = _line_mode()
|
||||
main = state.current_sweep_processed if line_mode == "processed" else raw
|
||||
if main is not None:
|
||||
curve.setData(xs[: main.size], main, autoDownsample=True)
|
||||
else:
|
||||
curve.setData([], [])
|
||||
if line_mode == "raw":
|
||||
if state.calib_mode == "file" and state.calib_file_envelope is not None:
|
||||
upper = np.asarray(state.calib_file_envelope, dtype=np.float32)
|
||||
n_env = min(xs.size, upper.size)
|
||||
if n_env > 0:
|
||||
x_env = xs[:n_env]
|
||||
y_env = upper[:n_env]
|
||||
curve_env_lo.setData(x_env, -y_env, autoDownsample=True)
|
||||
curve_env_hi.setData(x_env, y_env, autoDownsample=True)
|
||||
else:
|
||||
curve_env_lo.setData([], [])
|
||||
curve_env_hi.setData([], [])
|
||||
elif state.last_calib_sweep is not None:
|
||||
calib = np.asarray(state.last_calib_sweep, dtype=np.float32)
|
||||
lower, upper = build_calib_envelopes(calib)
|
||||
n_env = min(xs.size, upper.size, lower.size)
|
||||
if n_env > 0:
|
||||
curve_env_lo.setData(xs[:n_env], lower[:n_env], autoDownsample=True)
|
||||
curve_env_hi.setData(xs[:n_env], upper[:n_env], autoDownsample=True)
|
||||
else:
|
||||
curve_env_lo.setData([], [])
|
||||
curve_env_hi.setData([], [])
|
||||
else:
|
||||
curve_env_lo.setData([], [])
|
||||
curve_env_hi.setData([], [])
|
||||
else:
|
||||
curve_env_lo.setData([], [])
|
||||
curve_env_hi.setData([], [])
|
||||
@ -328,16 +547,19 @@ def run_pyqtgraph(args):
|
||||
post = state.current_sweep_post_exp if state.current_sweep_post_exp is not None else raw
|
||||
curve_post_exp.setData(xs[: post.size], post, autoDownsample=True)
|
||||
|
||||
if state.current_sweep_processed is not None:
|
||||
proc = state.current_sweep_processed
|
||||
curve.setData(xs[: proc.size], proc, autoDownsample=True)
|
||||
if line_mode == "processed":
|
||||
if state.current_sweep_processed is not None:
|
||||
proc = state.current_sweep_processed
|
||||
curve.setData(xs[: proc.size], proc, autoDownsample=True)
|
||||
else:
|
||||
curve.setData([], [])
|
||||
else:
|
||||
curve.setData([], [])
|
||||
curve.setData(xs[: raw.size], raw, autoDownsample=True)
|
||||
curve_norm.setData([], [])
|
||||
else:
|
||||
curve_pre_exp.setData([], [])
|
||||
curve_post_exp.setData([], [])
|
||||
if state.current_sweep_norm is not None:
|
||||
if line_mode == "raw" and state.current_sweep_norm is not None:
|
||||
curve_norm.setData(
|
||||
xs[: state.current_sweep_norm.size],
|
||||
state.current_sweep_norm,
|
||||
@ -385,6 +607,8 @@ def run_pyqtgraph(args):
|
||||
except Exception:
|
||||
pass
|
||||
ch_text.setText(state.format_channel_label())
|
||||
elif changed:
|
||||
_refresh_pipeline_label()
|
||||
|
||||
# Водопад спектров — новые данные справа (без реверса)
|
||||
if changed and ring.is_ready:
|
||||
|
||||
Reference in New Issue
Block a user