diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index 916f05b..6bc435e 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -3,6 +3,7 @@ from __future__ import annotations import signal +import sys import threading import time 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 RAW_PLOT_MAX_POINTS = 4096 +RAW_WATERFALL_MAX_POINTS = 2048 DEBUG_FRAME_LOG_EVERY = 10 @@ -603,7 +605,7 @@ def run_pyqtgraph(args) -> None: if finite_f.size > 0: f_min = float(np.min(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)) p_img.setRange( xRange=(0, max_sweeps - 1), @@ -714,7 +716,7 @@ def run_pyqtgraph(args) -> None: runtime.current_peak_width = None runtime.current_peak_amplitude = None 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) update_physical_axes() @@ -1242,6 +1244,7 @@ def run_pyqtgraph(args) -> None: pass processed_frames = 0 + ui_started_at = time.perf_counter() def refresh_current_fft_cache(sweep_for_fft: np.ndarray, bins: int) -> None: runtime.current_fft_complex = compute_fft_complex_row( @@ -1311,10 +1314,13 @@ def run_pyqtgraph(args) -> None: queue_size = queue.qsize() except Exception: 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( - "[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, + frames_per_sec, str(info.get("sweep") if isinstance(info, dict) else None), str(info.get("ch") if isinstance(info, dict) else None), int(getattr(sweep, "size", 0)), @@ -1640,7 +1646,7 @@ def run_pyqtgraph(args) -> None: runtime.plot_dirty = False 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) if levels is not None: img.setImage(disp, autoLevels=False, levels=levels) diff --git a/rfg_adc_plotter/io/sweep_reader.py b/rfg_adc_plotter/io/sweep_reader.py index d3ca71c..79d0010 100644 --- a/rfg_adc_plotter/io/sweep_reader.py +++ b/rfg_adc_plotter/io/sweep_reader.py @@ -101,6 +101,7 @@ class SweepReader(threading.Thread): self._src: SerialLineSource | None = None self._frames_read = 0 self._frames_dropped = 0 + self._started_at = time.perf_counter() def _build_parser(self): if self._parser_complex_ascii: @@ -175,12 +176,15 @@ class SweepReader(threading.Thread): queue_size = self._queue.qsize() except Exception: 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 channel = info.get("ch") if isinstance(info, dict) else None 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, + frames_per_sec, str(sweep_idx), str(channel), int(getattr(sweep, "size", 0)), diff --git a/rfg_adc_plotter/state/ring_buffer.py b/rfg_adc_plotter/state/ring_buffer.py index 86430d9..84b13a6 100644 --- a/rfg_adc_plotter/state/ring_buffer.py +++ b/rfg_adc_plotter/state/ring_buffer.py @@ -201,6 +201,21 @@ class RingBuffer: base = self.ring if self.head == 0 else np.roll(self.ring, -self.head, axis=0) 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: if self.ring_fft is None: return np.zeros((1, 1), dtype=np.float32) diff --git a/tests/test_ring_buffer.py b/tests/test_ring_buffer.py index 0a14397..9b1e41b 100644 --- a/tests/test_ring_buffer.py +++ b/tests/test_ring_buffer.py @@ -41,6 +41,22 @@ class RingBufferTests(unittest.TestCase): self.assertIsNotNone(ring.last_fft_db) 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): ring = RingBuffer(max_sweeps=2) sweep = np.linspace(0.0, 1.0, 64, dtype=np.float32)