a bit faster realisation. But waterfall fails when number of points in a row varyes
This commit is contained in:
@ -264,9 +264,25 @@ def main():
|
|||||||
default=None,
|
default=None,
|
||||||
help="Фиксированные Y-пределы для кривой формата min,max (например -1000,1000). По умолчанию авто",
|
help="Фиксированные Y-пределы для кривой формата min,max (например -1000,1000). По умолчанию авто",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--backend",
|
||||||
|
choices=["auto", "pg", "mpl"],
|
||||||
|
default="auto",
|
||||||
|
help="Графический бэкенд: pyqtgraph (pg) — быстрее; matplotlib (mpl) — совместимый. По умолчанию auto",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Попробуем быстрый бэкенд (pyqtgraph) при auto/pg
|
||||||
|
if args.backend in ("auto", "pg"):
|
||||||
|
try:
|
||||||
|
return run_pyqtgraph(args)
|
||||||
|
except Exception as e:
|
||||||
|
if args.backend == "pg":
|
||||||
|
sys.stderr.write(f"[error] PyQtGraph бэкенд недоступен: {e}\n")
|
||||||
|
sys.exit(1)
|
||||||
|
# При auto — тихо откатываемся на matplotlib
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import matplotlib
|
import matplotlib
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
@ -288,6 +304,7 @@ def main():
|
|||||||
|
|
||||||
# Состояние для отображения
|
# Состояние для отображения
|
||||||
current_sweep: Optional[np.ndarray] = None
|
current_sweep: Optional[np.ndarray] = None
|
||||||
|
x_shared: Optional[np.ndarray] = None
|
||||||
width: Optional[int] = None
|
width: Optional[int] = None
|
||||||
max_sweeps = int(max(10, args.max_sweeps))
|
max_sweeps = int(max(10, args.max_sweeps))
|
||||||
ring = None # type: Optional[np.ndarray]
|
ring = None # type: Optional[np.ndarray]
|
||||||
@ -327,10 +344,11 @@ def main():
|
|||||||
frames_since_ylim_update = 0
|
frames_since_ylim_update = 0
|
||||||
|
|
||||||
def ensure_buffer(w: int):
|
def ensure_buffer(w: int):
|
||||||
nonlocal ring, width, head
|
nonlocal ring, width, head, x_shared
|
||||||
if ring is not None and width == w:
|
if ring is not None and width == w:
|
||||||
return
|
return
|
||||||
width = w
|
width = w
|
||||||
|
x_shared = np.arange(w, dtype=np.int32)
|
||||||
ring = np.full((max_sweeps, w), np.nan, dtype=np.float32)
|
ring = np.full((max_sweeps, w), np.nan, dtype=np.float32)
|
||||||
head = 0
|
head = 0
|
||||||
# Обновляем изображение под новые размеры
|
# Обновляем изображение под новые размеры
|
||||||
@ -386,8 +404,8 @@ def main():
|
|||||||
|
|
||||||
# Обновление линии последнего свипа
|
# Обновление линии последнего свипа
|
||||||
if current_sweep is not None:
|
if current_sweep is not None:
|
||||||
x = np.arange(current_sweep.size, dtype=np.int32)
|
xs = x_shared if x_shared is not None and x_shared.size == current_sweep.size else np.arange(current_sweep.size, dtype=np.int32)
|
||||||
line_obj.set_data(x, current_sweep)
|
line_obj.set_data(xs, current_sweep)
|
||||||
# Лимиты по X постоянные под текущую ширину
|
# Лимиты по X постоянные под текущую ширину
|
||||||
ax_line.set_xlim(0, max(1, current_sweep.size - 1))
|
ax_line.set_xlim(0, max(1, current_sweep.size - 1))
|
||||||
# Y-лимиты: фиксированные либо периодическая автоподстройка
|
# Y-лимиты: фиксированные либо периодическая автоподстройка
|
||||||
@ -421,5 +439,156 @@ def main():
|
|||||||
reader.join(timeout=1.0)
|
reader.join(timeout=1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def run_pyqtgraph(args):
|
||||||
|
"""Быстрый GUI на PyQtGraph. Требует pyqtgraph и PyQt5/PySide6."""
|
||||||
|
try:
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from PyQt5 import QtCore, QtWidgets # noqa: F401
|
||||||
|
except Exception:
|
||||||
|
# Возможно установлена PySide6
|
||||||
|
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[np.ndarray] = Queue(maxsize=1000)
|
||||||
|
stop_event = threading.Event()
|
||||||
|
reader = SweepReader(args.port, args.baud, q, stop_event)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# PyQtGraph настройки
|
||||||
|
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))
|
||||||
|
p_line.setLabel("bottom", "X")
|
||||||
|
p_line.setLabel("left", "Y")
|
||||||
|
|
||||||
|
# Водопад (справа)
|
||||||
|
p_img = win.addPlot(row=0, col=1, title="Водопад (последние свипы)")
|
||||||
|
p_img.invertY(True) # 0 сверху, новые снизу
|
||||||
|
p_img.showGrid(x=False, y=False)
|
||||||
|
p_img.setLabel("bottom", "X")
|
||||||
|
p_img.setLabel("left", "Номер свипа (время →)")
|
||||||
|
img = pg.ImageItem()
|
||||||
|
p_img.addItem(img)
|
||||||
|
|
||||||
|
# Состояние
|
||||||
|
ring: Optional[np.ndarray] = None
|
||||||
|
head = 0
|
||||||
|
width: Optional[int] = None
|
||||||
|
x_shared: Optional[np.ndarray] = None
|
||||||
|
current_sweep: Optional[np.ndarray] = None
|
||||||
|
y_min, y_max = None, None
|
||||||
|
fixed_ylim: Optional[Tuple[float, float]] = None
|
||||||
|
if args.ylim:
|
||||||
|
try:
|
||||||
|
y0, y1 = args.ylim.split(",")
|
||||||
|
fixed_ylim = (float(y0), float(y1))
|
||||||
|
p_line.setYRange(fixed_ylim[0], fixed_ylim[1], padding=0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ensure_buffer(w: int):
|
||||||
|
nonlocal ring, head, width, x_shared
|
||||||
|
if ring is not None and width == w:
|
||||||
|
return
|
||||||
|
width = w
|
||||||
|
x_shared = np.arange(w, dtype=np.int32)
|
||||||
|
ring = np.full((max_sweeps, w), np.nan, dtype=np.float32)
|
||||||
|
head = 0
|
||||||
|
img.setImage(ring, autoLevels=False)
|
||||||
|
p_img.setRange(xRange=(0, max(1, w - 1)), yRange=(0, max_sweeps - 1), padding=0)
|
||||||
|
p_line.setXRange(0, max(1, w - 1), padding=0)
|
||||||
|
|
||||||
|
def push_sweep(s: np.ndarray):
|
||||||
|
nonlocal ring, head, y_min, y_max
|
||||||
|
if s is None or s.size == 0 or ring is None:
|
||||||
|
return
|
||||||
|
w = ring.shape[1]
|
||||||
|
row = np.full((w,), np.nan, dtype=np.float32)
|
||||||
|
take = min(w, s.size)
|
||||||
|
row[:take] = s[:take]
|
||||||
|
ring[head, :] = row
|
||||||
|
head = (head + 1) % ring.shape[0]
|
||||||
|
# Обновление экстремумов
|
||||||
|
sv_min = np.nanmin(row)
|
||||||
|
sv_max = np.nanmax(row)
|
||||||
|
if y_min is None or (not np.isnan(sv_min) and sv_min < y_min):
|
||||||
|
y_min = float(sv_min)
|
||||||
|
if y_max is None or (not np.isnan(sv_max) and sv_max > y_max):
|
||||||
|
y_max = float(sv_max)
|
||||||
|
|
||||||
|
def drain_queue():
|
||||||
|
nonlocal current_sweep
|
||||||
|
drained = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
s = q.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
break
|
||||||
|
drained += 1
|
||||||
|
current_sweep = s
|
||||||
|
ensure_buffer(s.size)
|
||||||
|
push_sweep(s)
|
||||||
|
return drained
|
||||||
|
|
||||||
|
# Попытка применить LUT из колормэпа (если доступен)
|
||||||
|
try:
|
||||||
|
cm_mod = getattr(pg, "colormap", None)
|
||||||
|
if cm_mod is not None:
|
||||||
|
cm = cm_mod.get(args.cmap)
|
||||||
|
img.setLookupTable(cm.getLookupTable(0.0, 1.0, 256))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update():
|
||||||
|
changed = drain_queue() > 0
|
||||||
|
if current_sweep is not None and x_shared is not None:
|
||||||
|
curve.setData(x_shared[: current_sweep.size], current_sweep, autoDownsample=True)
|
||||||
|
if fixed_ylim is None:
|
||||||
|
y0 = float(np.nanmin(current_sweep))
|
||||||
|
y1 = float(np.nanmax(current_sweep))
|
||||||
|
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)
|
||||||
|
|
||||||
|
if changed and ring is not None:
|
||||||
|
disp = ring if head == 0 else np.roll(ring, -head, axis=0)
|
||||||
|
if y_min is not None and y_max is not None and y_min != y_max and np.isfinite(y_min) and np.isfinite(y_max):
|
||||||
|
img.setImage(disp, autoLevels=False, levels=(y_min, y_max))
|
||||||
|
else:
|
||||||
|
img.setImage(disp, 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()
|
||||||
|
# На случай если aboutToQuit не сработал
|
||||||
|
on_quit()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user