This commit is contained in:
awe
2026-04-09 19:56:38 +03:00
parent 08823404c0
commit 4dbedb48bc
4 changed files with 46 additions and 5 deletions

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import signal import signal
import sys
import threading import threading
import time import time
from queue import Empty, Queue from queue import Empty, Queue
@ -38,6 +39,7 @@ from rfg_adc_plotter.state import RingBuffer, RuntimeState
from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo, SweepPacket from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo, SweepPacket
RAW_PLOT_MAX_POINTS = 4096 RAW_PLOT_MAX_POINTS = 4096
RAW_WATERFALL_MAX_POINTS = 2048
DEBUG_FRAME_LOG_EVERY = 10 DEBUG_FRAME_LOG_EVERY = 10
@ -603,7 +605,7 @@ def run_pyqtgraph(args) -> None:
if finite_f.size > 0: if finite_f.size > 0:
f_min = float(np.min(finite_f)) f_min = float(np.min(finite_f))
f_max = float(np.max(finite_f)) f_max = float(np.max(finite_f))
img.setImage(runtime.ring.get_display_raw(), autoLevels=False) img.setImage(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS), autoLevels=False)
img.setRect(0, f_min, max_sweeps, max(1e-9, f_max - f_min)) img.setRect(0, f_min, max_sweeps, max(1e-9, f_max - f_min))
p_img.setRange( p_img.setRange(
xRange=(0, max_sweeps - 1), xRange=(0, max_sweeps - 1),
@ -714,7 +716,7 @@ def run_pyqtgraph(args) -> None:
runtime.current_peak_width = None runtime.current_peak_width = None
runtime.current_peak_amplitude = None runtime.current_peak_amplitude = None
runtime.peak_candidates = [] runtime.peak_candidates = []
img.setImage(runtime.ring.get_display_raw(), autoLevels=False) img.setImage(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS), autoLevels=False)
img_fft.setImage(runtime.ring.get_display_fft_linear(), autoLevels=False) img_fft.setImage(runtime.ring.get_display_fft_linear(), autoLevels=False)
update_physical_axes() update_physical_axes()
@ -1242,6 +1244,7 @@ def run_pyqtgraph(args) -> None:
pass pass
processed_frames = 0 processed_frames = 0
ui_started_at = time.perf_counter()
def refresh_current_fft_cache(sweep_for_fft: np.ndarray, bins: int) -> None: def refresh_current_fft_cache(sweep_for_fft: np.ndarray, bins: int) -> None:
runtime.current_fft_complex = compute_fft_complex_row( runtime.current_fft_complex = compute_fft_complex_row(
@ -1311,10 +1314,13 @@ def run_pyqtgraph(args) -> None:
queue_size = queue.qsize() queue_size = queue.qsize()
except Exception: except Exception:
queue_size = -1 queue_size = -1
elapsed_s = max(time.perf_counter() - ui_started_at, 1e-9)
frames_per_sec = float(processed_frames) / elapsed_s
sys.stderr.write( sys.stderr.write(
"[debug] ui frames:%d last_sweep:%s ch:%s width:%d queue:%d\n" "[debug] ui frames:%d rate:%.2f/s last_sweep:%s ch:%s width:%d queue:%d\n"
% ( % (
processed_frames, processed_frames,
frames_per_sec,
str(info.get("sweep") if isinstance(info, dict) else None), str(info.get("sweep") if isinstance(info, dict) else None),
str(info.get("ch") if isinstance(info, dict) else None), str(info.get("ch") if isinstance(info, dict) else None),
int(getattr(sweep, "size", 0)), int(getattr(sweep, "size", 0)),
@ -1640,7 +1646,7 @@ def run_pyqtgraph(args) -> None:
runtime.plot_dirty = False runtime.plot_dirty = False
if changed and runtime.ring.ring is not None: if changed and runtime.ring.ring is not None:
disp = runtime.ring.get_display_raw() disp = runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS)
levels = _visible_levels_pyqtgraph(disp, p_img) levels = _visible_levels_pyqtgraph(disp, p_img)
if levels is not None: if levels is not None:
img.setImage(disp, autoLevels=False, levels=levels) img.setImage(disp, autoLevels=False, levels=levels)

View File

@ -101,6 +101,7 @@ class SweepReader(threading.Thread):
self._src: SerialLineSource | None = None self._src: SerialLineSource | None = None
self._frames_read = 0 self._frames_read = 0
self._frames_dropped = 0 self._frames_dropped = 0
self._started_at = time.perf_counter()
def _build_parser(self): def _build_parser(self):
if self._parser_complex_ascii: if self._parser_complex_ascii:
@ -175,12 +176,15 @@ class SweepReader(threading.Thread):
queue_size = self._queue.qsize() queue_size = self._queue.qsize()
except Exception: except Exception:
queue_size = -1 queue_size = -1
elapsed_s = max(time.perf_counter() - self._started_at, 1e-9)
frames_per_sec = float(self._frames_read) / elapsed_s
sweep_idx = info.get("sweep") if isinstance(info, dict) else None sweep_idx = info.get("sweep") if isinstance(info, dict) else None
channel = info.get("ch") if isinstance(info, dict) else None channel = info.get("ch") if isinstance(info, dict) else None
sys.stderr.write( sys.stderr.write(
"[debug] reader frames:%d last_sweep:%s ch:%s width:%d queue:%d dropped:%d\n" "[debug] reader frames:%d rate:%.2f/s last_sweep:%s ch:%s width:%d queue:%d dropped:%d\n"
% ( % (
self._frames_read, self._frames_read,
frames_per_sec,
str(sweep_idx), str(sweep_idx),
str(channel), str(channel),
int(getattr(sweep, "size", 0)), int(getattr(sweep, "size", 0)),

View File

@ -201,6 +201,21 @@ class RingBuffer:
base = self.ring if self.head == 0 else np.roll(self.ring, -self.head, axis=0) base = self.ring if self.head == 0 else np.roll(self.ring, -self.head, axis=0)
return base.T return base.T
def get_display_raw_decimated(self, max_points: int) -> np.ndarray:
"""Return a display-oriented raw waterfall with optional frequency decimation."""
if self.ring is None:
return np.zeros((1, 1), dtype=np.float32)
limit = int(max_points)
if limit <= 0 or self.width <= limit:
return self.get_display_raw()
row_order = np.arange(self.ring.shape[0], dtype=np.int64)
if self.head:
row_order = np.roll(row_order, -self.head)
col_idx = np.linspace(0, self.width - 1, limit, dtype=np.int64)
return self.ring[np.ix_(row_order, col_idx)].T
def get_display_fft_linear(self) -> np.ndarray: def get_display_fft_linear(self) -> np.ndarray:
if self.ring_fft is None: if self.ring_fft is None:
return np.zeros((1, 1), dtype=np.float32) return np.zeros((1, 1), dtype=np.float32)

View File

@ -41,6 +41,22 @@ class RingBufferTests(unittest.TestCase):
self.assertIsNotNone(ring.last_fft_db) self.assertIsNotNone(ring.last_fft_db)
self.assertEqual(ring.last_fft_db.shape, (ring.fft_bins,)) self.assertEqual(ring.last_fft_db.shape, (ring.fft_bins,))
def test_ring_buffer_can_return_decimated_display_raw(self):
ring = RingBuffer(max_sweeps=3)
sweep_a = np.linspace(0.0, 1.0, 4096, dtype=np.float32)
sweep_b = np.linspace(1.0, 2.0, 4096, dtype=np.float32)
sweep_c = np.linspace(2.0, 3.0, 4096, dtype=np.float32)
freqs = np.linspace(3.3, 14.3, 4096, dtype=np.float64)
ring.push(sweep_a, freqs)
ring.push(sweep_b, freqs)
ring.push(sweep_c, freqs)
raw = ring.get_display_raw_decimated(256)
self.assertEqual(raw.shape, (256, 3))
self.assertAlmostEqual(float(raw[0, -1]), float(sweep_c[0]), places=6)
self.assertAlmostEqual(float(raw[-1, -1]), float(sweep_c[-1]), places=6)
def test_ring_buffer_can_switch_fft_mode_and_rebuild_fft_rows(self): def test_ring_buffer_can_switch_fft_mode_and_rebuild_fft_rows(self):
ring = RingBuffer(max_sweeps=2) ring = RingBuffer(max_sweeps=2)
sweep = np.linspace(0.0, 1.0, 64, dtype=np.float32) sweep = np.linspace(0.0, 1.0, 64, dtype=np.float32)