voltage range

This commit is contained in:
awe
2026-04-14 20:39:44 +03:00
parent d170fc11e5
commit 3cb3d1c31a
6 changed files with 148 additions and 22 deletions

View File

@ -74,7 +74,17 @@ def build_parser() -> argparse.ArgumentParser:
"8-байтный бинарный протокол: либо legacy старт " "8-байтный бинарный протокол: либо legacy старт "
"0xFFFF,0xFFFF,0xFFFF,(CH<<8)|0x0A и точки step,uint32(hi16,lo16),0x000A, " "0xFFFF,0xFFFF,0xFFFF,(CH<<8)|0x0A и точки step,uint32(hi16,lo16),0x000A, "
"либо tty CH1/CH2 поток из kamil_adc в формате 0x000A,step,ch1_i16,ch2_i16. " "либо tty CH1/CH2 поток из kamil_adc в формате 0x000A,step,ch1_i16,ch2_i16. "
"Для tty CH1/CH2: сырая кривая = ch1^2+ch2^2, FFT вход = ch1+i*ch2" "Для tty CH1/CH2: после парсинга int16 переводятся в В, "
"сырая кривая = ch1^2+ch2^2 (В^2), FFT вход = ch1+i*ch2 (В)"
),
)
parser.add_argument(
"--tty-range-v",
type=float,
default=5.0,
help=(
"Полный диапазон для пересчета tty int16 в напряжение ±V "
"(только для --bin CH1/CH2, по умолчанию 5.0)"
), ),
) )
parser.add_argument( parser.add_argument(

View File

@ -61,6 +61,10 @@ DEFAULT_MAIN_WINDOW_WIDTH = 1200
DEFAULT_MAIN_WINDOW_HEIGHT = 680 DEFAULT_MAIN_WINDOW_HEIGHT = 680
MIN_MAIN_WINDOW_WIDTH = 640 MIN_MAIN_WINDOW_WIDTH = 640
MIN_MAIN_WINDOW_HEIGHT = 420 MIN_MAIN_WINDOW_HEIGHT = 420
TTY_CODE_SCALE_DENOM = 32767.0
TTY_RANGE_DEFAULT_V = 5.0
TTY_RANGE_MIN_V = 1e-6
TTY_RANGE_MAX_V = 1_000_000.0
def sanitize_curve_data_for_display( def sanitize_curve_data_for_display(
@ -489,6 +493,28 @@ def compute_aux_phase_curve(aux_curves: SweepAuxCurves) -> Optional[np.ndarray]:
return phase return phase
def sanitize_tty_voltage_range(range_v: float, default: float = TTY_RANGE_DEFAULT_V) -> float:
"""Return a finite positive full-scale voltage range for tty int16 conversion."""
try:
value = float(range_v)
except Exception:
value = float(default)
if not np.isfinite(value):
value = float(default)
return float(np.clip(abs(value), TTY_RANGE_MIN_V, TTY_RANGE_MAX_V))
def convert_tty_i16_to_voltage(codes: np.ndarray, range_v: float) -> np.ndarray:
"""Convert signed tty int16 code array to clipped voltage values in ``[-range_v, +range_v]``."""
code_arr = np.asarray(codes, dtype=np.float32).reshape(-1)
if code_arr.size <= 0:
return np.zeros((0,), dtype=np.float32)
range_abs_v = sanitize_tty_voltage_range(range_v)
scale_v = range_abs_v / float(TTY_CODE_SCALE_DENOM)
volt = code_arr * np.float32(scale_v)
return np.clip(volt, -range_abs_v, range_abs_v).astype(np.float32, copy=False)
def decimate_curve_for_display( def decimate_curve_for_display(
xs: Optional[np.ndarray], xs: Optional[np.ndarray],
ys: Optional[np.ndarray], ys: Optional[np.ndarray],
@ -606,6 +632,7 @@ def run_pyqtgraph(args) -> None:
peak_calibrate_mode = bool(getattr(args, "calibrate", False)) peak_calibrate_mode = bool(getattr(args, "calibrate", False))
peak_search_enabled = bool(getattr(args, "peak_search", False)) peak_search_enabled = bool(getattr(args, "peak_search", False))
bin_mode = bool(getattr(args, "bin_mode", False)) bin_mode = bool(getattr(args, "bin_mode", False))
tty_range_v = sanitize_tty_voltage_range(getattr(args, "tty_range_v", TTY_RANGE_DEFAULT_V))
complex_ascii_mode = bool(getattr(args, "parser_complex_ascii", False)) complex_ascii_mode = bool(getattr(args, "parser_complex_ascii", False))
complex_sweep_mode = bool( complex_sweep_mode = bool(
bin_mode bin_mode
@ -695,7 +722,7 @@ def run_pyqtgraph(args) -> None:
p_line_aux_vb = pg.ViewBox() p_line_aux_vb = pg.ViewBox()
try: try:
p_line.showAxis("right") p_line.showAxis("right")
p_line.getAxis("right").setLabel("CH1/CH2") p_line.getAxis("right").setLabel("CH1/CH2, В")
p_line.scene().addItem(p_line_aux_vb) p_line.scene().addItem(p_line_aux_vb)
p_line.getAxis("right").linkToView(p_line_aux_vb) p_line.getAxis("right").linkToView(p_line_aux_vb)
p_line_aux_vb.setXLink(p_line) p_line_aux_vb.setXLink(p_line)
@ -718,7 +745,7 @@ def run_pyqtgraph(args) -> None:
p_line.setLabel("left", "Y") p_line.setLabel("left", "Y")
if bin_iq_power_mode: if bin_iq_power_mode:
try: try:
p_line.setLabel("left", "CH1^2 + CH2^2") p_line.setLabel("left", "CH1^2 + CH2^2, В^2")
except Exception: except Exception:
pass pass
ch_text = pg.TextItem("", anchor=(1, 1)) ch_text = pg.TextItem("", anchor=(1, 1))
@ -790,7 +817,7 @@ def run_pyqtgraph(args) -> None:
try: try:
if bin_iq_power_mode: if bin_iq_power_mode:
p_fft.setTitle("FFT: CH1 + i*CH2") p_fft.setTitle("FFT: CH1 + i*CH2")
p_line.setTitle("Сырые CH1/CH2 и CH1^2 + CH2^2") p_line.setTitle("Сырые CH1/CH2 (В) и CH1^2 + CH2^2 (В^2)")
else: else:
p_fft.setTitle("FFT: Re / Im / Abs") p_fft.setTitle("FFT: Re / Im / Abs")
p_line.setTitle("Сырые данные до FFT") p_line.setTitle("Сырые данные до FFT")
@ -820,7 +847,7 @@ def run_pyqtgraph(args) -> None:
curve_complex_calib_real = p_complex_calib.plot(pen=pg.mkPen((80, 120, 255), width=1)) curve_complex_calib_real = p_complex_calib.plot(pen=pg.mkPen((80, 120, 255), width=1))
curve_complex_calib_imag = p_complex_calib.plot(pen=pg.mkPen((120, 200, 120), width=1)) curve_complex_calib_imag = p_complex_calib.plot(pen=pg.mkPen((120, 200, 120), width=1))
p_complex_calib.setLabel("bottom", "ГГц") p_complex_calib.setLabel("bottom", "ГГц")
p_complex_calib.setLabel("left", "Амплитуда") p_complex_calib.setLabel("left", "В" if bin_iq_power_mode else "Амплитуда")
try: try:
p_complex_calib.setXLink(p_line) p_complex_calib.setXLink(p_line)
p_complex_calib.setVisible(bool(complex_sweep_mode)) p_complex_calib.setVisible(bool(complex_sweep_mode))
@ -945,10 +972,28 @@ def run_pyqtgraph(args) -> None:
background_buttons_row.addWidget(background_save_btn) background_buttons_row.addWidget(background_save_btn)
background_buttons_row.addWidget(background_load_btn) background_buttons_row.addWidget(background_load_btn)
background_group_layout.addLayout(background_buttons_row) background_group_layout.addLayout(background_buttons_row)
tty_range_group = QtWidgets.QGroupBox("TTY диапазон")
tty_range_layout = QtWidgets.QFormLayout(tty_range_group)
tty_range_layout.setContentsMargins(6, 6, 6, 6)
tty_range_layout.setSpacing(6)
tty_range_spin = QtWidgets.QDoubleSpinBox()
tty_range_spin.setDecimals(6)
tty_range_spin.setRange(TTY_RANGE_MIN_V, TTY_RANGE_MAX_V)
tty_range_spin.setSingleStep(0.1)
try:
tty_range_spin.setSuffix(" V")
except Exception:
pass
tty_range_spin.setValue(tty_range_v)
tty_range_layout.addRow("±V", tty_range_spin)
try:
tty_range_group.setEnabled(bool(bin_iq_power_mode))
except Exception:
pass
parsed_data_cb = QtWidgets.QCheckBox("данные после парсинга") parsed_data_cb = QtWidgets.QCheckBox("данные после парсинга")
if complex_sweep_mode: if complex_sweep_mode:
try: try:
parsed_data_cb.setText("Сырые CH1/CH2" if bin_iq_power_mode else "Сырые Re/Im") parsed_data_cb.setText("Сырые CH1/CH2 (В)" if bin_iq_power_mode else "Сырые Re/Im")
parsed_data_cb.setChecked(True) parsed_data_cb.setChecked(True)
except Exception: except Exception:
pass pass
@ -978,6 +1023,7 @@ def run_pyqtgraph(args) -> None:
settings_layout.addWidget(range_group) settings_layout.addWidget(range_group)
settings_layout.addWidget(calib_group) settings_layout.addWidget(calib_group)
settings_layout.addWidget(complex_calib_group) settings_layout.addWidget(complex_calib_group)
settings_layout.addWidget(tty_range_group)
settings_layout.addWidget(parsed_data_cb) settings_layout.addWidget(parsed_data_cb)
settings_layout.addWidget(background_group) settings_layout.addWidget(background_group)
settings_layout.addWidget(fft_mode_label) settings_layout.addWidget(fft_mode_label)
@ -1005,6 +1051,7 @@ def run_pyqtgraph(args) -> None:
status_dirty = True status_dirty = True
calibration_toggle_in_progress = False calibration_toggle_in_progress = False
range_change_in_progress = False range_change_in_progress = False
tty_range_change_in_progress = False
debug_event_counts: Dict[str, int] = {} debug_event_counts: Dict[str, int] = {}
last_queue_backlog = 0 last_queue_backlog = 0
last_backlog_skipped = 0 last_backlog_skipped = 0
@ -1164,6 +1211,27 @@ def run_pyqtgraph(args) -> None:
path = "" path = ""
return path or "fft_background.npy" return path or "fft_background.npy"
def rebuild_tty_voltage_curves_from_codes() -> bool:
if (not bin_iq_power_mode) or runtime.full_current_aux_curves_codes is None:
return False
try:
code_1, code_2 = runtime.full_current_aux_curves_codes
except Exception:
return False
code_1_arr = np.asarray(code_1, dtype=np.float32).reshape(-1)
code_2_arr = np.asarray(code_2, dtype=np.float32).reshape(-1)
width = min(code_1_arr.size, code_2_arr.size)
if width <= 0:
return False
ch_1_v = convert_tty_i16_to_voltage(code_1_arr[:width], tty_range_v)
ch_2_v = convert_tty_i16_to_voltage(code_2_arr[:width], tty_range_v)
runtime.full_current_aux_curves = (ch_1_v, ch_2_v)
runtime.full_current_fft_source = ch_1_v.astype(np.complex64) + (1j * ch_2_v.astype(np.complex64))
ch_1_v_f64 = ch_1_v.astype(np.float64, copy=False)
ch_2_v_f64 = ch_2_v.astype(np.float64, copy=False)
runtime.full_current_sweep_raw = np.asarray((ch_1_v_f64 * ch_1_v_f64) + (ch_2_v_f64 * ch_2_v_f64), dtype=np.float32)
return True
def reset_background_state(*, clear_profile: bool = True) -> None: def reset_background_state(*, clear_profile: bool = True) -> None:
runtime.background_buffer.reset() runtime.background_buffer.reset()
if clear_profile: if clear_profile:
@ -1435,6 +1503,33 @@ def run_pyqtgraph(args) -> None:
set_status_note(f"диапазон: {new_min:.6g}..{new_max:.6g} GHz") set_status_note(f"диапазон: {new_min:.6g}..{new_max:.6g} GHz")
runtime.mark_dirty() runtime.mark_dirty()
def set_tty_range() -> None:
nonlocal tty_range_v, tty_range_change_in_progress
if tty_range_change_in_progress:
return
if not bin_iq_power_mode:
return
try:
requested_v = float(tty_range_spin.value())
except Exception:
requested_v = tty_range_v
new_range_v = sanitize_tty_voltage_range(requested_v, default=tty_range_v)
if np.isclose(new_range_v, tty_range_v, rtol=0.0, atol=1e-12):
return
tty_range_v = new_range_v
tty_range_change_in_progress = True
try:
tty_range_spin.setValue(tty_range_v)
finally:
tty_range_change_in_progress = False
if rebuild_tty_voltage_curves_from_codes():
reset_background_state(clear_profile=True)
refresh_current_window(push_to_ring=True, reset_ring=True)
set_status_note(f"tty диапазон: ±{tty_range_v:.6g} В")
else:
set_status_note(f"tty диапазон: ±{tty_range_v:.6g} В (ожидание данных)")
runtime.mark_dirty()
def pick_calib_file() -> None: def pick_calib_file() -> None:
start_path = get_calib_file_path() start_path = get_calib_file_path()
try: try:
@ -1687,6 +1782,7 @@ def run_pyqtgraph(args) -> None:
except Exception: except Exception:
pass pass
restore_range_controls() restore_range_controls()
set_tty_range()
set_calib_enabled() set_calib_enabled()
set_complex_calib_enabled() set_complex_calib_enabled()
set_parsed_data_enabled() set_parsed_data_enabled()
@ -1698,6 +1794,7 @@ def run_pyqtgraph(args) -> None:
try: try:
range_min_spin.valueChanged.connect(lambda _v: set_working_range()) range_min_spin.valueChanged.connect(lambda _v: set_working_range())
range_max_spin.valueChanged.connect(lambda _v: set_working_range()) range_max_spin.valueChanged.connect(lambda _v: set_working_range())
tty_range_spin.valueChanged.connect(lambda _v: set_tty_range())
calib_cb.stateChanged.connect(lambda _v: set_calib_enabled()) calib_cb.stateChanged.connect(lambda _v: set_calib_enabled())
complex_calib_cb.stateChanged.connect(lambda _v: set_complex_calib_enabled()) complex_calib_cb.stateChanged.connect(lambda _v: set_complex_calib_enabled())
parsed_data_cb.stateChanged.connect(lambda _v: set_parsed_data_enabled()) parsed_data_cb.stateChanged.connect(lambda _v: set_parsed_data_enabled())
@ -1935,6 +2032,7 @@ def run_pyqtgraph(args) -> None:
) )
base_freqs = np.linspace(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ, sweep.size, dtype=np.float64) base_freqs = np.linspace(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ, sweep.size, dtype=np.float64)
runtime.full_current_aux_curves = None runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.full_current_fft_source = None runtime.full_current_fft_source = None
if complex_sweep_mode and aux_curves is not None: if complex_sweep_mode and aux_curves is not None:
try: try:
@ -1944,21 +2042,20 @@ def run_pyqtgraph(args) -> None:
runtime.full_current_freqs = np.asarray(calibrated_aux_1_payload["F"], dtype=np.float64) runtime.full_current_freqs = np.asarray(calibrated_aux_1_payload["F"], dtype=np.float64)
calibrated_aux_1 = np.asarray(calibrated_aux_1_payload["I"], dtype=np.float32) calibrated_aux_1 = np.asarray(calibrated_aux_1_payload["I"], dtype=np.float32)
calibrated_aux_2 = np.asarray(calibrated_aux_2, dtype=np.float32) calibrated_aux_2 = np.asarray(calibrated_aux_2, dtype=np.float32)
runtime.full_current_aux_curves = (calibrated_aux_1, calibrated_aux_2)
runtime.full_current_fft_source = (
calibrated_aux_1.astype(np.complex64) + (1j * calibrated_aux_2.astype(np.complex64))
)
if bin_iq_power_mode: if bin_iq_power_mode:
aux_1_f64 = calibrated_aux_1.astype(np.float64, copy=False) runtime.full_current_aux_curves_codes = (calibrated_aux_1, calibrated_aux_2)
aux_2_f64 = calibrated_aux_2.astype(np.float64, copy=False) if not rebuild_tty_voltage_curves_from_codes():
runtime.full_current_sweep_raw = np.asarray( runtime.full_current_aux_curves = None
(aux_1_f64 * aux_1_f64) + (aux_2_f64 * aux_2_f64), runtime.full_current_fft_source = None
dtype=np.float32,
)
else: else:
runtime.full_current_aux_curves = (calibrated_aux_1, calibrated_aux_2)
runtime.full_current_fft_source = (
calibrated_aux_1.astype(np.complex64) + (1j * calibrated_aux_2.astype(np.complex64))
)
runtime.full_current_sweep_raw = np.abs(runtime.full_current_fft_source).astype(np.float32) runtime.full_current_sweep_raw = np.abs(runtime.full_current_fft_source).astype(np.float32)
except Exception: except Exception:
runtime.full_current_aux_curves = None runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.full_current_fft_source = None runtime.full_current_fft_source = None
if runtime.full_current_fft_source is None: if runtime.full_current_fft_source is None:
@ -1976,12 +2073,16 @@ def run_pyqtgraph(args) -> None:
aux_1, aux_2 = aux_curves aux_1, aux_2 = aux_curves
calibrated_aux_1 = calibrate_freqs({"F": base_freqs, "I": aux_1})["I"] calibrated_aux_1 = calibrate_freqs({"F": base_freqs, "I": aux_1})["I"]
calibrated_aux_2 = calibrate_freqs({"F": base_freqs, "I": aux_2})["I"] calibrated_aux_2 = calibrate_freqs({"F": base_freqs, "I": aux_2})["I"]
runtime.full_current_aux_curves = ( calibrated_aux_1_arr = np.asarray(calibrated_aux_1, dtype=np.float32)
np.asarray(calibrated_aux_1, dtype=np.float32), calibrated_aux_2_arr = np.asarray(calibrated_aux_2, dtype=np.float32)
np.asarray(calibrated_aux_2, dtype=np.float32), if bin_iq_power_mode:
) runtime.full_current_aux_curves_codes = (calibrated_aux_1_arr, calibrated_aux_2_arr)
rebuild_tty_voltage_curves_from_codes()
else:
runtime.full_current_aux_curves = (calibrated_aux_1_arr, calibrated_aux_2_arr)
except Exception: except Exception:
runtime.full_current_aux_curves = None runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.current_info = info runtime.current_info = info
refresh_current_window(push_to_ring=True) refresh_current_window(push_to_ring=True)
processed_frames += 1 processed_frames += 1

View File

@ -36,7 +36,7 @@ def tty_ch_pair_to_sweep(ch_1: int, ch_2: int) -> float:
"""Reduce a raw CH1/CH2 TTY point to power-like scalar ``ch1^2 + ch2^2``.""" """Reduce a raw CH1/CH2 TTY point to power-like scalar ``ch1^2 + ch2^2``."""
ch_1_i = int(ch_1) ch_1_i = int(ch_1)
ch_2_i = int(ch_2) ch_2_i = int(ch_2)
return float(((ch_1_i * ch_1_i) + (ch_2_i * ch_2_i))**0.5) return float((ch_1_i * ch_1_i) + (ch_2_i * ch_2_i))
class AsciiSweepParser: class AsciiSweepParser:

View File

@ -22,6 +22,7 @@ class RuntimeState:
full_current_sweep_raw: Optional[np.ndarray] = None full_current_sweep_raw: Optional[np.ndarray] = None
full_current_fft_source: Optional[np.ndarray] = None full_current_fft_source: Optional[np.ndarray] = None
full_current_aux_curves: SweepAuxCurves = None full_current_aux_curves: SweepAuxCurves = None
full_current_aux_curves_codes: SweepAuxCurves = None
current_freqs: Optional[np.ndarray] = None current_freqs: Optional[np.ndarray] = None
current_distances: Optional[np.ndarray] = None current_distances: Optional[np.ndarray] = None
current_sweep_raw: Optional[np.ndarray] = None current_sweep_raw: Optional[np.ndarray] = None

View File

@ -26,10 +26,12 @@ class CliTests(unittest.TestCase):
args = build_parser().parse_args(["/dev/null"]) args = build_parser().parse_args(["/dev/null"])
self.assertFalse(args.logscale) self.assertFalse(args.logscale)
self.assertFalse(args.opengl) self.assertFalse(args.opengl)
self.assertAlmostEqual(float(args.tty_range_v), 5.0, places=6)
args_log = build_parser().parse_args(["/dev/null", "--logscale", "--opengl"]) args_log = build_parser().parse_args(["/dev/null", "--logscale", "--opengl", "--tty-range-v", "2.5"])
self.assertTrue(args_log.logscale) self.assertTrue(args_log.logscale)
self.assertTrue(args_log.opengl) self.assertTrue(args_log.opengl)
self.assertAlmostEqual(float(args_log.tty_range_v), 2.5, places=6)
def test_wrapper_help_works(self): def test_wrapper_help_works(self):
proc = _run("RFG_ADC_dataplotter.py", "--help") proc = _run("RFG_ADC_dataplotter.py", "--help")

View File

@ -14,6 +14,7 @@ from rfg_adc_plotter.gui.pyqtgraph_backend import (
coalesce_packets_for_ui, coalesce_packets_for_ui,
compute_background_subtracted_bscan_levels, compute_background_subtracted_bscan_levels,
compute_aux_phase_curve, compute_aux_phase_curve,
convert_tty_i16_to_voltage,
decimate_curve_for_display, decimate_curve_for_display,
resolve_axis_bounds, resolve_axis_bounds,
resolve_heavy_refresh_stride, resolve_heavy_refresh_stride,
@ -62,6 +63,17 @@ from rfg_adc_plotter.processing.peaks import find_peak_width_markers, find_top_p
class ProcessingTests(unittest.TestCase): class ProcessingTests(unittest.TestCase):
def test_convert_tty_i16_to_voltage_maps_and_clips_full_range(self):
codes = np.asarray([-32768.0, -16384.0, 0.0, 16384.0, 32767.0], dtype=np.float32)
volts = convert_tty_i16_to_voltage(codes, 5.0)
self.assertEqual(volts.shape, codes.shape)
self.assertAlmostEqual(float(volts[0]), -5.0, places=6)
self.assertAlmostEqual(float(volts[2]), 0.0, places=6)
self.assertAlmostEqual(float(volts[-1]), 5.0, places=6)
self.assertTrue(np.all(volts >= -5.0))
self.assertTrue(np.all(volts <= 5.0))
def test_recalculate_calibration_preserves_requested_edges(self): def test_recalculate_calibration_preserves_requested_edges(self):
coeffs = recalculate_calibration_c(np.asarray([0.0, 1.0, 0.025], dtype=np.float64), 3.3, 14.3) coeffs = recalculate_calibration_c(np.asarray([0.0, 1.0, 0.025], dtype=np.float64), 3.3, 14.3)
y0 = coeffs[0] + coeffs[1] * 3.3 + coeffs[2] * (3.3 ** 2) y0 = coeffs[0] + coeffs[1] * 3.3 + coeffs[2] * (3.3 ** 2)