new project structure
This commit is contained in:
0
rfg_adc_plotter/gui/__init__.py
Normal file
0
rfg_adc_plotter/gui/__init__.py
Normal file
284
rfg_adc_plotter/gui/matplotlib_backend.py
Normal file
284
rfg_adc_plotter/gui/matplotlib_backend.py
Normal file
@ -0,0 +1,284 @@
|
||||
"""Matplotlib-бэкенд реалтайм-плоттера свипов."""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from queue import Queue
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from rfg_adc_plotter.constants import FFT_LEN
|
||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||
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
|
||||
|
||||
|
||||
def _parse_ylim(ylim_str: Optional[str]) -> Optional[Tuple[float, float]]:
|
||||
if not ylim_str:
|
||||
return None
|
||||
try:
|
||||
y0, y1 = ylim_str.split(",")
|
||||
return (float(y0), float(y1))
|
||||
except Exception:
|
||||
sys.stderr.write("[warn] Некорректный формат --ylim, игнорирую. Ожидалось min,max\n")
|
||||
return None
|
||||
|
||||
|
||||
def _parse_spec_clip(spec: Optional[str]) -> Optional[Tuple[float, float]]:
|
||||
if not spec:
|
||||
return None
|
||||
s = str(spec).strip().lower()
|
||||
if s in ("off", "none", "no"):
|
||||
return None
|
||||
try:
|
||||
p0, p1 = s.replace(";", ",").split(",")
|
||||
low, high = float(p0), float(p1)
|
||||
if not (0.0 <= low < high <= 100.0):
|
||||
return None
|
||||
return (low, high)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _visible_levels(data: np.ndarray, axis) -> Optional[Tuple[float, float]]:
|
||||
"""(vmin, vmax) по текущей видимой области imshow."""
|
||||
if data.size == 0:
|
||||
return None
|
||||
ny, nx = data.shape[0], data.shape[1]
|
||||
try:
|
||||
x0, x1 = axis.get_xlim()
|
||||
y0, y1 = axis.get_ylim()
|
||||
except Exception:
|
||||
x0, x1 = 0.0, float(nx - 1)
|
||||
y0, y1 = 0.0, float(ny - 1)
|
||||
xmin, xmax = sorted((float(x0), float(x1)))
|
||||
ymin, ymax = sorted((float(y0), float(y1)))
|
||||
ix0 = max(0, min(nx - 1, int(np.floor(xmin))))
|
||||
ix1 = max(0, min(nx - 1, int(np.ceil(xmax))))
|
||||
iy0 = max(0, min(ny - 1, int(np.floor(ymin))))
|
||||
iy1 = max(0, min(ny - 1, int(np.ceil(ymax))))
|
||||
if ix1 < ix0:
|
||||
ix1 = ix0
|
||||
if iy1 < iy0:
|
||||
iy1 = iy0
|
||||
sub = data[iy0 : iy1 + 1, ix0 : ix1 + 1]
|
||||
finite = np.isfinite(sub)
|
||||
if not finite.any():
|
||||
return None
|
||||
vals = sub[finite]
|
||||
vmin = float(np.min(vals))
|
||||
vmax = float(np.max(vals))
|
||||
if not (np.isfinite(vmin) and np.isfinite(vmax)) or vmin == vmax:
|
||||
return None
|
||||
return (vmin, vmax)
|
||||
|
||||
|
||||
def run_matplotlib(args):
|
||||
try:
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.animation import FuncAnimation
|
||||
from matplotlib.widgets import CheckButtons, Slider
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[error] Нужны matplotlib и её зависимости: {e}\n")
|
||||
sys.exit(1)
|
||||
|
||||
q: Queue[SweepPacket] = Queue(maxsize=1000)
|
||||
stop_event = threading.Event()
|
||||
reader = SweepReader(args.port, args.baud, q, stop_event, fancy=bool(args.fancy))
|
||||
reader.start()
|
||||
|
||||
max_sweeps = int(max(10, args.max_sweeps))
|
||||
max_fps = max(1.0, float(args.max_fps))
|
||||
interval_ms = int(1000.0 / max_fps)
|
||||
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
|
||||
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
||||
fixed_ylim = _parse_ylim(getattr(args, "ylim", None))
|
||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
||||
|
||||
state = AppState(norm_type=norm_type)
|
||||
ring = RingBuffer(max_sweeps)
|
||||
|
||||
# --- Создание фигуры ---
|
||||
fig, axs = plt.subplots(2, 2, figsize=(12, 8))
|
||||
(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)
|
||||
|
||||
# Статусная строка
|
||||
status_text = fig.text(0.01, 0.01, "", ha="left", va="bottom", fontsize=8, family="monospace")
|
||||
|
||||
# График последнего свипа
|
||||
line_obj, = ax_line.plot([], [], lw=1, color="tab:blue")
|
||||
line_calib_obj, = ax_line.plot([], [], lw=1, color="tab:red")
|
||||
line_norm_obj, = ax_line.plot([], [], lw=1, color="tab:green")
|
||||
ax_line.set_title("Сырые данные", pad=1)
|
||||
ax_line.set_xlabel("F")
|
||||
channel_text = ax_line.text(
|
||||
0.98, 0.98, "", transform=ax_line.transAxes,
|
||||
ha="right", va="top", fontsize=9, family="monospace",
|
||||
)
|
||||
if fixed_ylim is not None:
|
||||
ax_line.set_ylim(fixed_ylim)
|
||||
|
||||
# График спектра
|
||||
fft_line_obj, = ax_fft.plot([], [], lw=1)
|
||||
ax_fft.set_title("FFT", pad=1)
|
||||
ax_fft.set_xlabel("X")
|
||||
ax_fft.set_ylabel("Амплитуда, дБ")
|
||||
|
||||
# Водопад сырых данных
|
||||
img_obj = ax_img.imshow(
|
||||
np.zeros((1, 1), dtype=np.float32),
|
||||
aspect="auto", interpolation="nearest", origin="lower", cmap=args.cmap,
|
||||
)
|
||||
ax_img.set_title("Сырые данные", pad=12)
|
||||
ax_img.set_ylabel("частота")
|
||||
try:
|
||||
ax_img.tick_params(axis="x", labelbottom=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Водопад спектров
|
||||
img_fft_obj = ax_spec.imshow(
|
||||
np.zeros((1, 1), dtype=np.float32),
|
||||
aspect="auto", interpolation="nearest", origin="lower", cmap=args.cmap,
|
||||
)
|
||||
ax_spec.set_title("B-scan (дБ)", pad=12)
|
||||
ax_spec.set_ylabel("расстояние")
|
||||
try:
|
||||
ax_spec.tick_params(axis="x", labelbottom=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Слайдеры и чекбокс
|
||||
contrast_slider = None
|
||||
try:
|
||||
fft_bins = ring.fft_bins
|
||||
ax_smin = fig.add_axes([0.92, 0.55, 0.02, 0.35])
|
||||
ax_smax = fig.add_axes([0.95, 0.55, 0.02, 0.35])
|
||||
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])
|
||||
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])
|
||||
|
||||
def _on_ylim_change(_val):
|
||||
try:
|
||||
y0 = int(min(ymin_slider.val, ymax_slider.val))
|
||||
y1 = int(max(ymin_slider.val, ymax_slider.val))
|
||||
ax_spec.set_ylim(y0, y1)
|
||||
fig.canvas.draw_idle()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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(lambda _v: state.set_calib_enabled(
|
||||
bool(calib_cb.get_status()[0])
|
||||
))
|
||||
except Exception:
|
||||
calib_cb = None
|
||||
|
||||
# --- Инициализация imshow при первом свипе ---
|
||||
def _init_imshow_extents():
|
||||
w = ring.width
|
||||
ms = ring.max_sweeps
|
||||
fb = ring.fft_bins
|
||||
img_obj.set_data(np.zeros((w, ms), dtype=np.float32))
|
||||
img_obj.set_extent((0, ms - 1, 0, w - 1 if w > 0 else 1))
|
||||
ax_img.set_xlim(0, ms - 1)
|
||||
ax_img.set_ylim(0, max(1, w - 1))
|
||||
img_fft_obj.set_data(np.zeros((fb, ms), dtype=np.float32))
|
||||
img_fft_obj.set_extent((0, ms - 1, 0, fb - 1))
|
||||
ax_spec.set_xlim(0, ms - 1)
|
||||
ax_spec.set_ylim(0, max(1, fb - 1))
|
||||
|
||||
_imshow_initialized = [False]
|
||||
|
||||
def update(_frame):
|
||||
changed = state.drain_queue(q, ring) > 0
|
||||
|
||||
if changed and not _imshow_initialized[0] and ring.is_ready:
|
||||
_init_imshow_extents()
|
||||
_imshow_initialized[0] = True
|
||||
|
||||
# Линейный график свипа
|
||||
if state.current_sweep_raw is not None:
|
||||
raw = state.current_sweep_raw
|
||||
if ring.x_shared is not None and raw.size <= ring.x_shared.size:
|
||||
xs = ring.x_shared[: raw.size]
|
||||
else:
|
||||
xs = np.arange(raw.size, dtype=np.int32)
|
||||
line_obj.set_data(xs, raw)
|
||||
if state.last_calib_sweep is not None:
|
||||
line_calib_obj.set_data(xs[: state.last_calib_sweep.size], state.last_calib_sweep)
|
||||
else:
|
||||
line_calib_obj.set_data([], [])
|
||||
if state.current_sweep_norm is not None:
|
||||
line_norm_obj.set_data(xs[: state.current_sweep_norm.size], state.current_sweep_norm)
|
||||
else:
|
||||
line_norm_obj.set_data([], [])
|
||||
ax_line.set_xlim(0, max(1, raw.size - 1))
|
||||
if fixed_ylim is None:
|
||||
y0 = float(np.nanmin(raw))
|
||||
y1 = float(np.nanmax(raw))
|
||||
if np.isfinite(y0) and np.isfinite(y1):
|
||||
if y0 == y1:
|
||||
pad = max(1.0, abs(y0) * 0.05)
|
||||
y0 -= pad
|
||||
y1 += pad
|
||||
else:
|
||||
pad = 0.05 * (y1 - y0)
|
||||
y0 -= pad
|
||||
y1 += pad
|
||||
ax_line.set_ylim(y0, y1)
|
||||
|
||||
# Спектр — используем уже вычисленный в ring FFT
|
||||
if ring.last_fft_vals is not None and ring.freq_shared is not None:
|
||||
fft_vals = ring.last_fft_vals
|
||||
xs_fft = ring.freq_shared
|
||||
if fft_vals.size > xs_fft.size:
|
||||
fft_vals = fft_vals[: xs_fft.size]
|
||||
fft_line_obj.set_data(xs_fft[: fft_vals.size], fft_vals)
|
||||
if np.isfinite(np.nanmin(fft_vals)) and np.isfinite(np.nanmax(fft_vals)):
|
||||
ax_fft.set_xlim(0, max(1, xs_fft.size - 1))
|
||||
ax_fft.set_ylim(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)))
|
||||
|
||||
# Водопад сырых данных
|
||||
if changed and ring.is_ready:
|
||||
disp = ring.get_display_ring()
|
||||
img_obj.set_data(disp)
|
||||
levels = _visible_levels(disp, ax_img)
|
||||
if levels is not None:
|
||||
img_obj.set_clim(vmin=levels[0], vmax=levels[1])
|
||||
|
||||
# Водопад спектров
|
||||
if changed and ring.is_ready:
|
||||
disp_fft = ring.get_display_ring_fft()
|
||||
disp_fft = ring.subtract_recent_mean_fft(disp_fft, spec_mean_sec)
|
||||
img_fft_obj.set_data(disp_fft)
|
||||
levels = ring.compute_fft_levels(disp_fft, spec_clip)
|
||||
if levels is not None:
|
||||
try:
|
||||
c = float(contrast_slider.val) / 100.0 if contrast_slider is not None else 1.0
|
||||
except Exception:
|
||||
c = 1.0
|
||||
vmax_eff = levels[0] + c * (levels[1] - levels[0])
|
||||
img_fft_obj.set_clim(vmin=levels[0], vmax=vmax_eff)
|
||||
|
||||
# Статус и подпись канала
|
||||
if changed and state.current_info:
|
||||
status_text.set_text(format_status(state.current_info))
|
||||
channel_text.set_text(state.format_channel_label())
|
||||
|
||||
return (line_obj, line_calib_obj, line_norm_obj, img_obj, fft_line_obj, img_fft_obj, status_text, channel_text)
|
||||
|
||||
ani = FuncAnimation(fig, update, interval=interval_ms, blit=False)
|
||||
plt.show()
|
||||
stop_event.set()
|
||||
reader.join(timeout=1.0)
|
||||
272
rfg_adc_plotter/gui/pyqtgraph_backend.py
Normal file
272
rfg_adc_plotter/gui/pyqtgraph_backend.py
Normal file
@ -0,0 +1,272 @@
|
||||
"""PyQtGraph-бэкенд реалтайм-плоттера свипов."""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from queue import Queue
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||
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
|
||||
|
||||
|
||||
def _parse_ylim(ylim_str: Optional[str]) -> Optional[Tuple[float, float]]:
|
||||
if not ylim_str:
|
||||
return None
|
||||
try:
|
||||
y0, y1 = ylim_str.split(",")
|
||||
return (float(y0), float(y1))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_spec_clip(spec: Optional[str]) -> Optional[Tuple[float, float]]:
|
||||
if not spec:
|
||||
return None
|
||||
s = str(spec).strip().lower()
|
||||
if s in ("off", "none", "no"):
|
||||
return None
|
||||
try:
|
||||
p0, p1 = s.replace(";", ",").split(",")
|
||||
low, high = float(p0), float(p1)
|
||||
if not (0.0 <= low < high <= 100.0):
|
||||
return None
|
||||
return (low, high)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _visible_levels(data: np.ndarray, plot_item) -> Optional[Tuple[float, float]]:
|
||||
"""(vmin, vmax) по текущей видимой области ImageItem."""
|
||||
if data.size == 0:
|
||||
return None
|
||||
ny, nx = data.shape[0], data.shape[1]
|
||||
try:
|
||||
(x0, x1), (y0, y1) = plot_item.viewRange()
|
||||
except Exception:
|
||||
x0, x1 = 0.0, float(nx - 1)
|
||||
y0, y1 = 0.0, float(ny - 1)
|
||||
xmin, xmax = sorted((float(x0), float(x1)))
|
||||
ymin, ymax = sorted((float(y0), float(y1)))
|
||||
ix0 = max(0, min(nx - 1, int(np.floor(xmin))))
|
||||
ix1 = max(0, min(nx - 1, int(np.ceil(xmax))))
|
||||
iy0 = max(0, min(ny - 1, int(np.floor(ymin))))
|
||||
iy1 = max(0, min(ny - 1, int(np.ceil(ymax))))
|
||||
if ix1 < ix0:
|
||||
ix1 = ix0
|
||||
if iy1 < iy0:
|
||||
iy1 = iy0
|
||||
sub = data[iy0 : iy1 + 1, ix0 : ix1 + 1]
|
||||
finite = np.isfinite(sub)
|
||||
if not finite.any():
|
||||
return None
|
||||
vals = sub[finite]
|
||||
vmin = float(np.min(vals))
|
||||
vmax = float(np.max(vals))
|
||||
if not (np.isfinite(vmin) and np.isfinite(vmax)) or vmin == vmax:
|
||||
return None
|
||||
return (vmin, vmax)
|
||||
|
||||
|
||||
def run_pyqtgraph(args):
|
||||
"""Быстрый GUI на PyQtGraph. Требует pyqtgraph и PyQt5/PySide6."""
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtWidgets # noqa: F401
|
||||
except Exception:
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from PySide6 import QtCore, QtWidgets # noqa: F401
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"pyqtgraph/PyQt5(PySide6) не найдены. Установите: pip install pyqtgraph PyQt5"
|
||||
) from e
|
||||
|
||||
q: Queue[SweepPacket] = Queue(maxsize=1000)
|
||||
stop_event = threading.Event()
|
||||
reader = SweepReader(args.port, args.baud, q, stop_event, fancy=bool(args.fancy))
|
||||
reader.start()
|
||||
|
||||
max_sweeps = int(max(10, args.max_sweeps))
|
||||
max_fps = max(1.0, float(args.max_fps))
|
||||
interval_ms = int(1000.0 / max_fps)
|
||||
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
|
||||
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
||||
fixed_ylim = _parse_ylim(getattr(args, "ylim", None))
|
||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
||||
|
||||
state = AppState(norm_type=norm_type)
|
||||
ring = RingBuffer(max_sweeps)
|
||||
|
||||
# --- Создание окна ---
|
||||
pg.setConfigOptions(useOpenGL=True, antialias=False)
|
||||
app = pg.mkQApp(args.title)
|
||||
win = pg.GraphicsLayoutWidget(show=True, title=args.title)
|
||||
win.resize(1200, 600)
|
||||
|
||||
# График последнего свипа (слева-сверху)
|
||||
p_line = win.addPlot(row=0, col=0, title="Сырые данные")
|
||||
p_line.showGrid(x=True, y=True, alpha=0.3)
|
||||
curve = p_line.plot(pen=pg.mkPen((80, 120, 255), width=1))
|
||||
curve_calib = p_line.plot(pen=pg.mkPen((220, 60, 60), width=1))
|
||||
curve_norm = p_line.plot(pen=pg.mkPen((60, 180, 90), width=1))
|
||||
p_line.setLabel("bottom", "X")
|
||||
p_line.setLabel("left", "Y")
|
||||
ch_text = pg.TextItem("", anchor=(1, 1))
|
||||
ch_text.setZValue(10)
|
||||
p_line.addItem(ch_text)
|
||||
if fixed_ylim is not None:
|
||||
p_line.setYRange(fixed_ylim[0], fixed_ylim[1], padding=0)
|
||||
|
||||
# Водопад (справа-сверху)
|
||||
p_img = win.addPlot(row=0, col=1, title="Сырые данные водопад")
|
||||
p_img.invertY(False)
|
||||
p_img.showGrid(x=False, y=False)
|
||||
p_img.setLabel("bottom", "Время (новое справа)")
|
||||
try:
|
||||
p_img.getAxis("bottom").setStyle(showValues=False)
|
||||
except Exception:
|
||||
pass
|
||||
p_img.setLabel("left", "X (0 снизу)")
|
||||
img = pg.ImageItem()
|
||||
p_img.addItem(img)
|
||||
|
||||
# Применяем LUT из цветовой карты
|
||||
try:
|
||||
cm = pg.colormap.get(args.cmap)
|
||||
img.setLookupTable(cm.getLookupTable(0.0, 1.0, 256))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# FFT (слева-снизу)
|
||||
p_fft = win.addPlot(row=1, col=0, title="FFT")
|
||||
p_fft.showGrid(x=True, y=True, alpha=0.3)
|
||||
curve_fft = p_fft.plot(pen=pg.mkPen((255, 120, 80), width=1))
|
||||
p_fft.setLabel("bottom", "Бин")
|
||||
p_fft.setLabel("left", "Амплитуда, дБ")
|
||||
|
||||
# Водопад спектров (справа-снизу)
|
||||
p_spec = win.addPlot(row=1, col=1, title="B-scan (дБ)")
|
||||
p_spec.invertY(True)
|
||||
p_spec.showGrid(x=False, y=False)
|
||||
p_spec.setLabel("bottom", "Время (новое справа)")
|
||||
try:
|
||||
p_spec.getAxis("bottom").setStyle(showValues=False)
|
||||
except Exception:
|
||||
pass
|
||||
p_spec.setLabel("left", "Бин (0 снизу)")
|
||||
img_fft = pg.ImageItem()
|
||||
p_spec.addItem(img_fft)
|
||||
|
||||
# Чекбокс калибровки
|
||||
calib_cb = QtWidgets.QCheckBox("калибровка")
|
||||
cb_proxy = QtWidgets.QGraphicsProxyWidget()
|
||||
cb_proxy.setWidget(calib_cb)
|
||||
win.addItem(cb_proxy, row=2, col=1)
|
||||
calib_cb.stateChanged.connect(lambda _v: state.set_calib_enabled(calib_cb.isChecked()))
|
||||
|
||||
# Статусная строка
|
||||
status = pg.LabelItem(justify="left")
|
||||
win.addItem(status, row=3, col=0, colspan=2)
|
||||
|
||||
_imshow_initialized = [False]
|
||||
|
||||
def _init_imshow_extents():
|
||||
w = ring.width
|
||||
ms = ring.max_sweeps
|
||||
fb = ring.fft_bins
|
||||
img.setImage(ring.ring.T, autoLevels=False)
|
||||
p_img.setRange(xRange=(0, ms - 1), yRange=(0, max(1, w - 1)), padding=0)
|
||||
p_line.setXRange(0, max(1, w - 1), padding=0)
|
||||
img_fft.setImage(ring.ring_fft.T, autoLevels=False)
|
||||
p_spec.setRange(xRange=(0, ms - 1), yRange=(0, max(1, fb - 1)), padding=0)
|
||||
p_fft.setXRange(0, max(1, fb - 1), padding=0)
|
||||
|
||||
def update():
|
||||
changed = state.drain_queue(q, ring) > 0
|
||||
|
||||
if changed and not _imshow_initialized[0] and ring.is_ready:
|
||||
_init_imshow_extents()
|
||||
_imshow_initialized[0] = True
|
||||
|
||||
# Линейный график свипа
|
||||
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.last_calib_sweep is not None:
|
||||
curve_calib.setData(xs[: state.last_calib_sweep.size], state.last_calib_sweep, autoDownsample=True)
|
||||
else:
|
||||
curve_calib.setData([], [])
|
||||
if state.current_sweep_norm is not None:
|
||||
curve_norm.setData(xs[: state.current_sweep_norm.size], state.current_sweep_norm, autoDownsample=True)
|
||||
else:
|
||||
curve_norm.setData([], [])
|
||||
if fixed_ylim is None:
|
||||
y0 = float(np.nanmin(raw))
|
||||
y1 = float(np.nanmax(raw))
|
||||
if np.isfinite(y0) and np.isfinite(y1):
|
||||
margin = 0.05 * max(1.0, (y1 - y0))
|
||||
p_line.setYRange(y0 - margin, y1 + margin, padding=0)
|
||||
|
||||
# Спектр — используем уже вычисленный в ring FFT
|
||||
if ring.last_fft_vals is not None and ring.freq_shared is not None:
|
||||
fft_vals = ring.last_fft_vals
|
||||
xs_fft = ring.freq_shared
|
||||
if fft_vals.size > xs_fft.size:
|
||||
fft_vals = fft_vals[: xs_fft.size]
|
||||
curve_fft.setData(xs_fft[: fft_vals.size], fft_vals)
|
||||
p_fft.setYRange(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)), padding=0)
|
||||
|
||||
# Позиция подписи канала
|
||||
try:
|
||||
(x0, x1), (y0, y1) = p_line.viewRange()
|
||||
dx = 0.01 * max(1.0, float(x1 - x0))
|
||||
dy = 0.01 * max(1.0, float(y1 - y0))
|
||||
ch_text.setPos(float(x1 - dx), float(y1 - dy))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Водопад сырых данных — новые данные справа (без реверса)
|
||||
if changed and ring.is_ready:
|
||||
disp = ring.get_display_ring() # (width, time), новые справа
|
||||
levels = _visible_levels(disp, p_img)
|
||||
if levels is not None:
|
||||
img.setImage(disp, autoLevels=False, levels=levels)
|
||||
else:
|
||||
img.setImage(disp, autoLevels=False)
|
||||
|
||||
# Статус и подпись канала
|
||||
if changed and state.current_info:
|
||||
try:
|
||||
status.setText(format_status(state.current_info))
|
||||
except Exception:
|
||||
pass
|
||||
ch_text.setText(state.format_channel_label())
|
||||
|
||||
# Водопад спектров — новые данные справа (без реверса)
|
||||
if changed and ring.is_ready:
|
||||
disp_fft = ring.get_display_ring_fft() # (bins, time), новые справа
|
||||
disp_fft = ring.subtract_recent_mean_fft(disp_fft, spec_mean_sec)
|
||||
levels = ring.compute_fft_levels(disp_fft, spec_clip)
|
||||
if levels is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||
else:
|
||||
img_fft.setImage(disp_fft, autoLevels=False)
|
||||
|
||||
timer = pg.QtCore.QTimer()
|
||||
timer.timeout.connect(update)
|
||||
timer.start(interval_ms)
|
||||
|
||||
def on_quit():
|
||||
stop_event.set()
|
||||
reader.join(timeout=1.0)
|
||||
|
||||
app.aboutToQuit.connect(on_quit)
|
||||
win.show()
|
||||
exec_fn = getattr(app, "exec_", None) or getattr(app, "exec", None)
|
||||
exec_fn()
|
||||
on_quit()
|
||||
Reference in New Issue
Block a user