This commit is contained in:
awe
2026-03-12 15:12:20 +03:00
parent 3cc423031c
commit c2a892f397
27 changed files with 3200 additions and 0 deletions

View File

@ -0,0 +1,6 @@
"""Runtime state helpers."""
from rfg_adc_plotter.state.ring_buffer import RingBuffer
from rfg_adc_plotter.state.runtime_state import RuntimeState
__all__ = ["RingBuffer", "RuntimeState"]

View File

@ -0,0 +1,106 @@
"""Ring buffers for raw sweeps and FFT waterfall rows."""
from __future__ import annotations
import time
from typing import Optional
import numpy as np
from rfg_adc_plotter.constants import FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ, WF_WIDTH
from rfg_adc_plotter.processing.fft import compute_distance_axis, compute_fft_mag_row, fft_mag_to_db
class RingBuffer:
"""Store raw sweeps, FFT rows, and matching time markers."""
def __init__(self, max_sweeps: int):
self.max_sweeps = int(max_sweeps)
self.fft_bins = FFT_LEN // 2 + 1
self.width = 0
self.head = 0
self.ring: Optional[np.ndarray] = None
self.ring_time: Optional[np.ndarray] = None
self.ring_fft: Optional[np.ndarray] = None
self.x_shared: Optional[np.ndarray] = None
self.distance_axis: Optional[np.ndarray] = None
self.last_fft_db: Optional[np.ndarray] = None
self.y_min_fft: Optional[float] = None
self.y_max_fft: Optional[float] = None
@property
def is_ready(self) -> bool:
return self.ring is not None and self.ring_fft is not None
def ensure_init(self, sweep_width: int) -> bool:
"""Allocate or resize buffers. Returns True when geometry changed."""
target_width = max(int(sweep_width), int(WF_WIDTH))
changed = False
if self.ring is None or self.ring_time is None or self.ring_fft is None:
self.width = target_width
self.ring = np.full((self.max_sweeps, self.width), np.nan, dtype=np.float32)
self.ring_time = np.full((self.max_sweeps,), np.nan, dtype=np.float64)
self.ring_fft = np.full((self.max_sweeps, self.fft_bins), np.nan, dtype=np.float32)
self.head = 0
changed = True
elif target_width != self.width:
new_ring = np.full((self.max_sweeps, target_width), np.nan, dtype=np.float32)
take = min(self.width, target_width)
new_ring[:, :take] = self.ring[:, :take]
self.ring = new_ring
self.width = target_width
changed = True
if self.x_shared is None or self.x_shared.size != self.width:
self.x_shared = np.linspace(
SWEEP_FREQ_MIN_GHZ,
SWEEP_FREQ_MAX_GHZ,
self.width,
dtype=np.float32,
)
changed = True
return changed
def push(self, sweep: np.ndarray, freqs: Optional[np.ndarray] = None) -> None:
"""Push a processed sweep and refresh raw/FFT buffers."""
if sweep is None or sweep.size == 0:
return
self.ensure_init(int(sweep.size))
if self.ring is None or self.ring_time is None or self.ring_fft is None:
return
row = np.full((self.width,), np.nan, dtype=np.float32)
take = min(self.width, int(sweep.size))
row[:take] = np.asarray(sweep[:take], dtype=np.float32)
self.ring[self.head, :] = row
self.ring_time[self.head] = time.time()
fft_mag = compute_fft_mag_row(sweep, freqs, self.fft_bins)
self.ring_fft[self.head, :] = fft_mag
self.last_fft_db = fft_mag_to_db(fft_mag)
if self.last_fft_db.size > 0:
fr_min = float(np.nanmin(self.last_fft_db))
fr_max = float(np.nanmax(self.last_fft_db))
self.y_min_fft = fr_min if self.y_min_fft is None else min(self.y_min_fft, fr_min)
self.y_max_fft = fr_max if self.y_max_fft is None else max(self.y_max_fft, fr_max)
self.distance_axis = compute_distance_axis(freqs, self.fft_bins)
self.head = (self.head + 1) % self.max_sweeps
def get_display_raw(self) -> np.ndarray:
if self.ring is None:
return np.zeros((1, 1), dtype=np.float32)
base = self.ring if self.head == 0 else np.roll(self.ring, -self.head, axis=0)
return base.T
def get_display_fft_linear(self) -> np.ndarray:
if self.ring_fft is None:
return np.zeros((1, 1), dtype=np.float32)
base = self.ring_fft if self.head == 0 else np.roll(self.ring_fft, -self.head, axis=0)
return base.T
def get_display_times(self) -> Optional[np.ndarray]:
if self.ring_time is None:
return None
return self.ring_time if self.head == 0 else np.roll(self.ring_time, -self.head)

View File

@ -0,0 +1,32 @@
"""Mutable state container for the PyQtGraph backend."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import numpy as np
from rfg_adc_plotter.state.ring_buffer import RingBuffer
from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo
@dataclass
class RuntimeState:
ring: RingBuffer
current_freqs: Optional[np.ndarray] = None
current_distances: Optional[np.ndarray] = None
current_sweep_raw: Optional[np.ndarray] = None
current_aux_curves: SweepAuxCurves = None
current_sweep_norm: Optional[np.ndarray] = None
current_fft_db: Optional[np.ndarray] = None
last_calib_sweep: Optional[np.ndarray] = None
current_info: Optional[SweepInfo] = None
bg_spec_cache: Optional[np.ndarray] = None
current_peak_width: Optional[float] = None
current_peak_amplitude: Optional[float] = None
peak_candidates: List[Dict[str, float]] = field(default_factory=list)
plot_dirty: bool = False
def mark_dirty(self) -> None:
self.plot_dirty = True