test new variant

This commit is contained in:
awe
2026-04-28 19:32:10 +03:00
parent ffb7dc3f25
commit 9ff97bf737
11 changed files with 813 additions and 33 deletions

View File

@ -73,9 +73,12 @@ def build_parser() -> argparse.ArgumentParser:
help=(
"8-байтный бинарный протокол: либо legacy старт "
"0xFFFF,0xFFFF,0xFFFF,(CH<<8)|0x0A и точки step,uint32(hi16,lo16),0x000A, "
"либо mixed поток 0x000A,step,ch1_i16,ch2_i16 и 0x001A,step,data_i16,0x0000. "
"либо mixed поток 0x000A,step,ch1_i16,ch2_i16; "
"0x00A3/0x00A4,step,ch1_i16,ch2_i16 (DO1 LOW/HIGH tagged); "
"и 0x001A,step,data_i16,0x0000. "
"Для 0x000A: после парсинга int16 переводятся в В, "
"сырая кривая = ch1^2+ch2^2 (В^2), FFT вход = ch1+i*ch2 (В). "
"Для 0x00A3/0x00A4: auto-detect tagged режим с раздельным отображением LOW/HIGH в raw. "
"Для 0x001A: code_i16 переводится в В, raw = V, FFT вход = exp(V)"
),
)

View File

@ -8,7 +8,7 @@ import sys
import threading
import time
from queue import Empty, Queue
from typing import Dict, List, Optional, Sequence, Tuple
from typing import Any, Dict, List, Optional, Sequence, Tuple
import numpy as np
@ -67,6 +67,7 @@ TTY_RANGE_DEFAULT_V = 5.0
TTY_RANGE_MIN_V = 1e-6
TTY_RANGE_MAX_V = 1_000_000.0
LOGDET_EXP_INPUT_LIMIT = 80.0
DO1_TAGGED_INFO_KEY = "_do1_tagged_payload"
def sanitize_curve_data_for_display(
@ -491,6 +492,50 @@ def resolve_visible_aux_curves(aux_curves: SweepAuxCurves, enabled: bool) -> Swe
return aux_1_arr, aux_2_arr
def compute_do1_tagged_aggregate(
raw_low: Optional[np.ndarray],
raw_high: Optional[np.ndarray],
) -> Optional[np.ndarray]:
"""Build a merged raw series from LOW/HIGH tagged curves using nan-aware averaging."""
if raw_low is None and raw_high is None:
return None
if raw_low is None:
return np.asarray(raw_high, dtype=np.float32).reshape(-1)
if raw_high is None:
return np.asarray(raw_low, dtype=np.float32).reshape(-1)
low_arr = np.asarray(raw_low, dtype=np.float32).reshape(-1)
high_arr = np.asarray(raw_high, dtype=np.float32).reshape(-1)
width = min(low_arr.size, high_arr.size)
if width <= 0:
return np.zeros((0,), dtype=np.float32)
low_arr = low_arr[:width]
high_arr = high_arr[:width]
out = np.full((width,), np.nan, dtype=np.float32)
low_valid = np.isfinite(low_arr)
high_valid = np.isfinite(high_arr)
both_valid = low_valid & high_valid
low_only = low_valid & (~high_valid)
high_only = high_valid & (~low_valid)
out[low_only] = low_arr[low_only]
out[high_only] = high_arr[high_only]
out[both_valid] = (low_arr[both_valid] + high_arr[both_valid]) * 0.5
return out
def resolve_visible_do1_tagged_aux_curves(
aux_low: SweepAuxCurves,
aux_high: SweepAuxCurves,
enabled: bool,
) -> Tuple[SweepAuxCurves, SweepAuxCurves]:
"""Return visible LOW/HIGH CH1/CH2 pairs for the DO1 tagged raw mode."""
if not enabled:
return (None, None)
return (
resolve_visible_aux_curves(aux_low, enabled=True),
resolve_visible_aux_curves(aux_high, enabled=True),
)
def compute_aux_phase_curve(aux_curves: SweepAuxCurves) -> Optional[np.ndarray]:
"""Compute phase-like curve atan2(aux_2, aux_1) for raw CH2/CH1 display."""
if aux_curves is None:
@ -508,6 +553,17 @@ def compute_aux_phase_curve(aux_curves: SweepAuxCurves) -> Optional[np.ndarray]:
return phase
def compute_do1_tagged_phase_curves(
aux_low: SweepAuxCurves,
aux_high: SweepAuxCurves,
) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]:
"""Compute separate phase curves for DO1 LOW/HIGH tagged auxiliary data."""
return (
compute_aux_phase_curve(aux_low),
compute_aux_phase_curve(aux_high),
)
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:
@ -810,6 +866,8 @@ def run_pyqtgraph(args) -> None:
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))
curve_raw_low = p_line.plot(pen=pg.mkPen((255, 90, 90), width=1))
curve_raw_high = p_line.plot(pen=pg.mkPen((90, 220, 255), width=1))
p_line_aux_vb = None
if bin_iq_power_mode:
p_line_aux_vb = pg.ViewBox()
@ -823,15 +881,23 @@ def run_pyqtgraph(args) -> None:
p_line_aux_vb = None
curve_aux_1 = pg.PlotDataItem(pen=pg.mkPen((255, 170, 40), width=1))
curve_aux_2 = pg.PlotDataItem(pen=pg.mkPen((170, 70, 255), width=1))
curve_aux_3 = pg.PlotDataItem(pen=pg.mkPen((255, 120, 20), width=1))
curve_aux_4 = pg.PlotDataItem(pen=pg.mkPen((120, 60, 220), width=1))
if p_line_aux_vb is not None:
p_line_aux_vb.addItem(curve_aux_1)
p_line_aux_vb.addItem(curve_aux_2)
p_line_aux_vb.addItem(curve_aux_3)
p_line_aux_vb.addItem(curve_aux_4)
else:
p_line.addItem(curve_aux_1)
p_line.addItem(curve_aux_2)
p_line.addItem(curve_aux_3)
p_line.addItem(curve_aux_4)
else:
curve_aux_1 = p_line.plot(pen=pg.mkPen((255, 170, 40), width=1))
curve_aux_2 = p_line.plot(pen=pg.mkPen((170, 70, 255), width=1))
curve_aux_3 = p_line.plot(pen=pg.mkPen((255, 120, 20), width=1))
curve_aux_4 = p_line.plot(pen=pg.mkPen((120, 60, 220), width=1))
curve_calib = p_line.plot(pen=pg.mkPen((220, 60, 60), width=1))
curve_norm = p_line.plot(pen=pg.mkPen((60, 180, 90), width=1))
p_line.setLabel("bottom", "ГГц")
@ -864,6 +930,7 @@ def run_pyqtgraph(args) -> None:
p_line_phase = win.addPlot(row=2, col=0, title="Raw phase: atan(CH2/CH1)")
p_line_phase.showGrid(x=True, y=True, alpha=0.3)
curve_phase = p_line_phase.plot(pen=pg.mkPen((230, 180, 40), width=1))
curve_phase_high = p_line_phase.plot(pen=pg.mkPen((80, 220, 220), width=1))
p_line_phase.setLabel("bottom", "ГГц")
p_line_phase.setLabel("left", "рад")
try:
@ -1312,18 +1379,23 @@ def run_pyqtgraph(args) -> None:
if not isinstance(payload, dict):
return None
signal_kind = payload.get("signal_kind")
if signal_kind in {"bin_iq", "bin_logdet"}:
if signal_kind in {"bin_iq", "bin_logdet", "bin_iq_do1_tagged"}:
return str(signal_kind)
return None
def current_packet_is_do1_tagged(info: Optional[SweepInfo] = None) -> bool:
return get_signal_kind(info) == "bin_iq_do1_tagged"
def current_packet_is_complex() -> bool:
return bool(complex_sweep_mode) and get_signal_kind() != "bin_logdet"
signal_kind = get_signal_kind()
return bool(complex_sweep_mode) and signal_kind not in {"bin_logdet", "bin_iq_do1_tagged"}
def refresh_signal_mode_labels() -> None:
signal_kind = get_signal_kind()
active_complex = current_packet_is_complex()
is_logdet = signal_kind == "bin_logdet"
is_bin_iq = signal_kind == "bin_iq"
is_do1_tagged = signal_kind == "bin_iq_do1_tagged"
try:
if is_logdet:
@ -1331,6 +1403,11 @@ def run_pyqtgraph(args) -> None:
p_line.setLabel("left", "В")
p_fft.setTitle("FFT: exp(V)")
parsed_data_cb.setText("Сырые log-detector (В)")
elif is_do1_tagged:
p_line.setTitle("DO1 tagged raw: LOW/HIGH CH1^2 + CH2^2 (В^2)")
p_line.setLabel("left", "CH1^2 + CH2^2, В^2")
p_fft.setTitle("FFT")
parsed_data_cb.setText("DO1 tagged CH1/CH2 (В)")
elif is_bin_iq:
p_line.setTitle("Сырые CH1/CH2 (В) и CH1^2 + CH2^2 (В^2)")
p_line.setLabel("left", "CH1^2 + CH2^2, В^2")
@ -1347,7 +1424,10 @@ def run_pyqtgraph(args) -> None:
p_fft.setTitle("FFT")
parsed_data_cb.setText("данные после парсинга")
p_fft.setLabel("left", "Амплитуда" if active_complex else "дБ")
p_complex_calib.setVisible(bool(active_complex))
p_img.setVisible(not is_do1_tagged)
p_fft.setVisible(not is_do1_tagged)
p_spec.setVisible(not is_do1_tagged)
p_complex_calib.setVisible((not is_do1_tagged) and bool(active_complex))
except Exception:
pass
@ -1365,6 +1445,12 @@ def run_pyqtgraph(args) -> None:
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_do1_tagged_raw_low = None
runtime.full_do1_tagged_raw_high = None
runtime.full_do1_tagged_aux_low = None
runtime.full_do1_tagged_aux_high = None
runtime.full_do1_tagged_aux_low_codes = None
runtime.full_do1_tagged_aux_high_codes = None
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)
@ -1373,6 +1459,44 @@ def run_pyqtgraph(args) -> None:
runtime.full_current_sweep_codes = None
return True
def rebuild_do1_tagged_voltage_curves_from_codes() -> bool:
if (not bin_iq_power_mode) or runtime.full_do1_tagged_aux_low_codes is None or runtime.full_do1_tagged_aux_high_codes is None:
return False
try:
low_code_1, low_code_2 = runtime.full_do1_tagged_aux_low_codes
high_code_1, high_code_2 = runtime.full_do1_tagged_aux_high_codes
except Exception:
return False
low_code_1_arr = np.asarray(low_code_1, dtype=np.float32).reshape(-1)
low_code_2_arr = np.asarray(low_code_2, dtype=np.float32).reshape(-1)
high_code_1_arr = np.asarray(high_code_1, dtype=np.float32).reshape(-1)
high_code_2_arr = np.asarray(high_code_2, dtype=np.float32).reshape(-1)
width = min(low_code_1_arr.size, low_code_2_arr.size, high_code_1_arr.size, high_code_2_arr.size)
if width <= 0:
return False
low_ch_1_v = convert_tty_i16_to_voltage(low_code_1_arr[:width], tty_range_v)
low_ch_2_v = convert_tty_i16_to_voltage(low_code_2_arr[:width], tty_range_v)
high_ch_1_v = convert_tty_i16_to_voltage(high_code_1_arr[:width], tty_range_v)
high_ch_2_v = convert_tty_i16_to_voltage(high_code_2_arr[:width], tty_range_v)
low_ch_1_v_f64 = low_ch_1_v.astype(np.float64, copy=False)
low_ch_2_v_f64 = low_ch_2_v.astype(np.float64, copy=False)
high_ch_1_v_f64 = high_ch_1_v.astype(np.float64, copy=False)
high_ch_2_v_f64 = high_ch_2_v.astype(np.float64, copy=False)
raw_low = np.asarray((low_ch_1_v_f64 * low_ch_1_v_f64) + (low_ch_2_v_f64 * low_ch_2_v_f64), dtype=np.float32)
raw_high = np.asarray((high_ch_1_v_f64 * high_ch_1_v_f64) + (high_ch_2_v_f64 * high_ch_2_v_f64), dtype=np.float32)
runtime.full_do1_tagged_raw_low = raw_low
runtime.full_do1_tagged_raw_high = raw_high
runtime.full_do1_tagged_aux_low = (low_ch_1_v, low_ch_2_v)
runtime.full_do1_tagged_aux_high = (high_ch_1_v, high_ch_2_v)
runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.full_current_sweep_codes = None
runtime.full_current_fft_source = None
runtime.full_current_sweep_raw = compute_do1_tagged_aggregate(raw_low, raw_high)
return runtime.full_current_sweep_raw is not None and runtime.full_current_sweep_raw.size > 0
def rebuild_logdet_voltage_curve_from_codes() -> bool:
if (not bin_iq_power_mode) or runtime.full_current_sweep_codes is None:
return False
@ -1380,6 +1504,12 @@ def run_pyqtgraph(args) -> None:
if code_arr.size <= 0:
return False
sweep_raw_v, fft_input = build_logdet_voltage_fft_input(code_arr, tty_range_v)
runtime.full_do1_tagged_raw_low = None
runtime.full_do1_tagged_raw_high = None
runtime.full_do1_tagged_aux_low = None
runtime.full_do1_tagged_aux_high = None
runtime.full_do1_tagged_aux_low_codes = None
runtime.full_do1_tagged_aux_high_codes = None
runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.full_current_sweep_raw = sweep_raw_v
@ -1390,6 +1520,8 @@ def run_pyqtgraph(args) -> None:
signal_kind = get_signal_kind()
if signal_kind == "bin_logdet":
return rebuild_logdet_voltage_curve_from_codes()
if signal_kind == "bin_iq_do1_tagged":
return rebuild_do1_tagged_voltage_curves_from_codes()
return rebuild_tty_voltage_curves_from_codes()
def reset_background_state(*, clear_profile: bool = True) -> None:
@ -1427,6 +1559,7 @@ def run_pyqtgraph(args) -> None:
if reset_ring:
reset_ring_buffers()
tagged_mode = current_packet_is_do1_tagged()
if runtime.full_current_freqs is None or runtime.full_current_sweep_raw is None:
runtime.current_freqs = None
runtime.current_sweep_raw = None
@ -1434,6 +1567,10 @@ def run_pyqtgraph(args) -> None:
runtime.current_fft_input = None
runtime.current_fft_complex = None
runtime.current_aux_curves = None
runtime.current_do1_tagged_raw_low = None
runtime.current_do1_tagged_raw_high = None
runtime.current_do1_tagged_aux_low = None
runtime.current_do1_tagged_aux_high = None
runtime.current_sweep_norm = None
runtime.current_fft_mag = None
runtime.current_fft_db = None
@ -1462,6 +1599,40 @@ def run_pyqtgraph(args) -> None:
runtime.range_min_ghz,
runtime.range_max_ghz,
)
if tagged_mode:
runtime.current_do1_tagged_raw_low = apply_working_range_to_signal(
runtime.full_current_freqs,
runtime.full_current_sweep_raw,
runtime.full_do1_tagged_raw_low,
runtime.range_min_ghz,
runtime.range_max_ghz,
)
runtime.current_do1_tagged_raw_high = apply_working_range_to_signal(
runtime.full_current_freqs,
runtime.full_current_sweep_raw,
runtime.full_do1_tagged_raw_high,
runtime.range_min_ghz,
runtime.range_max_ghz,
)
runtime.current_do1_tagged_aux_low = apply_working_range_to_aux_curves(
runtime.full_current_freqs,
runtime.full_current_sweep_raw,
runtime.full_do1_tagged_aux_low,
runtime.range_min_ghz,
runtime.range_max_ghz,
)
runtime.current_do1_tagged_aux_high = apply_working_range_to_aux_curves(
runtime.full_current_freqs,
runtime.full_current_sweep_raw,
runtime.full_do1_tagged_aux_high,
runtime.range_min_ghz,
runtime.range_max_ghz,
)
else:
runtime.current_do1_tagged_raw_low = None
runtime.current_do1_tagged_raw_high = None
runtime.current_do1_tagged_aux_low = None
runtime.current_do1_tagged_aux_high = None
if runtime.current_sweep_raw.size == 0:
if push_to_ring:
@ -1472,6 +1643,10 @@ def run_pyqtgraph(args) -> None:
runtime.current_fft_input = None
runtime.current_fft_complex = None
runtime.current_aux_curves = None
runtime.current_do1_tagged_raw_low = None
runtime.current_do1_tagged_raw_high = None
runtime.current_do1_tagged_aux_low = None
runtime.current_do1_tagged_aux_high = None
runtime.current_sweep_norm = None
runtime.current_fft_mag = None
runtime.current_fft_db = None
@ -1482,6 +1657,16 @@ def run_pyqtgraph(args) -> None:
recompute_current_processed_sweep(push_to_ring=push_to_ring)
def recompute_current_processed_sweep(push_to_ring: bool = False) -> None:
if current_packet_is_do1_tagged():
runtime.current_sweep_norm = None
runtime.current_fft_source = None
runtime.current_fft_input = None
runtime.current_fft_complex = None
runtime.current_fft_mag = None
runtime.current_fft_db = None
runtime.current_distances = None
return
fft_source = runtime.current_fft_source
if fft_source is None and runtime.current_sweep_raw is not None:
fft_source = np.asarray(runtime.current_sweep_raw, dtype=np.float32)
@ -2207,8 +2392,76 @@ def run_pyqtgraph(args) -> None:
runtime.full_current_aux_curves_codes = None
runtime.full_current_sweep_codes = None
runtime.full_current_fft_source = None
runtime.full_do1_tagged_raw_low = None
runtime.full_do1_tagged_raw_high = None
runtime.full_do1_tagged_aux_low = None
runtime.full_do1_tagged_aux_high = None
runtime.full_do1_tagged_aux_low_codes = None
runtime.full_do1_tagged_aux_high_codes = None
signal_kind = get_signal_kind(info)
if signal_kind == "bin_logdet":
if signal_kind == "bin_iq_do1_tagged":
calibrated = calibrate_freqs(
{
"F": base_freqs,
"I": sweep,
}
)
runtime.full_current_freqs = np.asarray(calibrated["F"], dtype=np.float64)
payload = info.get(DO1_TAGGED_INFO_KEY) if isinstance(info, dict) else None
if isinstance(payload, dict):
raw_low_payload = payload.get("raw_low")
raw_high_payload = payload.get("raw_high")
if raw_low_payload is not None:
runtime.full_do1_tagged_raw_low = np.asarray(
calibrate_freqs({"F": base_freqs, "I": raw_low_payload})["I"],
dtype=np.float32,
)
if raw_high_payload is not None:
runtime.full_do1_tagged_raw_high = np.asarray(
calibrate_freqs({"F": base_freqs, "I": raw_high_payload})["I"],
dtype=np.float32,
)
runtime.full_current_sweep_raw = compute_do1_tagged_aggregate(
runtime.full_do1_tagged_raw_low,
runtime.full_do1_tagged_raw_high,
)
aux_low_payload = payload.get("aux_low")
aux_high_payload = payload.get("aux_high")
if (
bin_iq_power_mode
and isinstance(aux_low_payload, (tuple, list))
and len(aux_low_payload) == 2
and isinstance(aux_high_payload, (tuple, list))
and len(aux_high_payload) == 2
):
try:
low_aux_1 = np.asarray(
calibrate_freqs({"F": base_freqs, "I": aux_low_payload[0]})["I"],
dtype=np.float32,
)
low_aux_2 = np.asarray(
calibrate_freqs({"F": base_freqs, "I": aux_low_payload[1]})["I"],
dtype=np.float32,
)
high_aux_1 = np.asarray(
calibrate_freqs({"F": base_freqs, "I": aux_high_payload[0]})["I"],
dtype=np.float32,
)
high_aux_2 = np.asarray(
calibrate_freqs({"F": base_freqs, "I": aux_high_payload[1]})["I"],
dtype=np.float32,
)
runtime.full_do1_tagged_aux_low_codes = (low_aux_1, low_aux_2)
runtime.full_do1_tagged_aux_high_codes = (high_aux_1, high_aux_2)
rebuild_do1_tagged_voltage_curves_from_codes()
except Exception:
runtime.full_do1_tagged_aux_low_codes = None
runtime.full_do1_tagged_aux_high_codes = None
if runtime.full_current_sweep_raw is None:
runtime.full_current_sweep_raw = np.asarray(calibrated["I"], dtype=np.float32)
runtime.full_current_fft_source = None
elif signal_kind == "bin_logdet":
calibrated = calibrate_freqs(
{
"F": base_freqs,
@ -2242,7 +2495,7 @@ def run_pyqtgraph(args) -> None:
runtime.full_current_aux_curves_codes = None
runtime.full_current_fft_source = None
if runtime.full_current_fft_source is None:
if (signal_kind != "bin_iq_do1_tagged") and runtime.full_current_fft_source is None:
calibrated = calibrate_freqs(
{
"F": base_freqs,
@ -2327,11 +2580,13 @@ def run_pyqtgraph(args) -> None:
or last_bscan_refresh_stride <= 1
or (update_ticks % last_bscan_refresh_stride) == 0
)
do1_tagged_now = current_packet_is_do1_tagged()
if redraw_needed:
refresh_signal_mode_labels()
active_signal_kind = get_signal_kind()
active_complex_mode = current_packet_is_complex()
active_do1_tagged = active_signal_kind == "bin_iq_do1_tagged"
xs = resolve_curve_xs(
runtime.current_sweep_raw.size
if runtime.current_sweep_raw is not None
@ -2339,20 +2594,78 @@ def run_pyqtgraph(args) -> None:
)
displayed_calib = None
displayed_aux = resolve_visible_aux_curves(runtime.current_aux_curves, parsed_data_enabled)
displayed_phase = compute_aux_phase_curve(runtime.current_aux_curves)
displayed_tagged_aux_low, displayed_tagged_aux_high = resolve_visible_do1_tagged_aux_curves(
runtime.current_do1_tagged_aux_low,
runtime.current_do1_tagged_aux_high,
parsed_data_enabled,
)
if active_do1_tagged:
displayed_phase, displayed_phase_high = compute_do1_tagged_phase_curves(
runtime.current_do1_tagged_aux_low,
runtime.current_do1_tagged_aux_high,
)
else:
displayed_phase = compute_aux_phase_curve(runtime.current_aux_curves)
displayed_phase_high = None
try:
p_line_phase.setVisible(displayed_phase is not None and displayed_phase.size > 0)
p_line_phase.setVisible(
(displayed_phase is not None and displayed_phase.size > 0)
or (displayed_phase_high is not None and displayed_phase_high.size > 0)
)
except Exception:
pass
if runtime.current_sweep_raw is not None:
if active_do1_tagged:
curve.setData([], [])
if runtime.current_do1_tagged_raw_low is not None:
raw_low_x, raw_low_y = decimate_curve_for_display(xs, runtime.current_do1_tagged_raw_low)
raw_low_x, raw_low_y = sanitize_curve_data_for_display(raw_low_x, raw_low_y)
curve_raw_low.setData(raw_low_x, raw_low_y, autoDownsample=False)
else:
curve_raw_low.setData([], [])
if runtime.current_do1_tagged_raw_high is not None:
raw_high_x, raw_high_y = decimate_curve_for_display(xs, runtime.current_do1_tagged_raw_high)
raw_high_x, raw_high_y = sanitize_curve_data_for_display(raw_high_x, raw_high_y)
curve_raw_high.setData(raw_high_x, raw_high_y, autoDownsample=False)
else:
curve_raw_high.setData([], [])
elif runtime.current_sweep_raw is not None:
raw_x, raw_y = decimate_curve_for_display(xs, runtime.current_sweep_raw)
raw_x, raw_y = sanitize_curve_data_for_display(raw_x, raw_y)
curve.setData(raw_x, raw_y, autoDownsample=False)
curve_raw_low.setData([], [])
curve_raw_high.setData([], [])
else:
curve.setData([], [])
curve_raw_low.setData([], [])
curve_raw_high.setData([], [])
if displayed_aux is not None:
if active_do1_tagged:
if displayed_tagged_aux_low is not None:
aux_low_1, aux_low_2 = displayed_tagged_aux_low
aux_low_width = min(xs.size, aux_low_1.size, aux_low_2.size)
low_x_1, low_y_1 = decimate_curve_for_display(xs[:aux_low_width], aux_low_1[:aux_low_width])
low_x_2, low_y_2 = decimate_curve_for_display(xs[:aux_low_width], aux_low_2[:aux_low_width])
low_x_1, low_y_1 = sanitize_curve_data_for_display(low_x_1, low_y_1)
low_x_2, low_y_2 = sanitize_curve_data_for_display(low_x_2, low_y_2)
curve_aux_1.setData(low_x_1, low_y_1, autoDownsample=False)
curve_aux_2.setData(low_x_2, low_y_2, autoDownsample=False)
else:
curve_aux_1.setData([], [])
curve_aux_2.setData([], [])
if displayed_tagged_aux_high is not None:
aux_high_1, aux_high_2 = displayed_tagged_aux_high
aux_high_width = min(xs.size, aux_high_1.size, aux_high_2.size)
high_x_1, high_y_1 = decimate_curve_for_display(xs[:aux_high_width], aux_high_1[:aux_high_width])
high_x_2, high_y_2 = decimate_curve_for_display(xs[:aux_high_width], aux_high_2[:aux_high_width])
high_x_1, high_y_1 = sanitize_curve_data_for_display(high_x_1, high_y_1)
high_x_2, high_y_2 = sanitize_curve_data_for_display(high_x_2, high_y_2)
curve_aux_3.setData(high_x_1, high_y_1, autoDownsample=False)
curve_aux_4.setData(high_x_2, high_y_2, autoDownsample=False)
else:
curve_aux_3.setData([], [])
curve_aux_4.setData([], [])
elif displayed_aux is not None:
aux_1, aux_2 = displayed_aux
aux_width = min(xs.size, aux_1.size, aux_2.size)
aux_x_1, aux_y_1 = decimate_curve_for_display(xs[:aux_width], aux_1[:aux_width])
@ -2361,9 +2674,13 @@ def run_pyqtgraph(args) -> None:
aux_x_2, aux_y_2 = sanitize_curve_data_for_display(aux_x_2, aux_y_2)
curve_aux_1.setData(aux_x_1, aux_y_1, autoDownsample=False)
curve_aux_2.setData(aux_x_2, aux_y_2, autoDownsample=False)
curve_aux_3.setData([], [])
curve_aux_4.setData([], [])
else:
curve_aux_1.setData([], [])
curve_aux_2.setData([], [])
curve_aux_3.setData([], [])
curve_aux_4.setData([], [])
if displayed_phase is not None:
phase_width = min(xs.size, displayed_phase.size)
@ -2372,8 +2689,18 @@ def run_pyqtgraph(args) -> None:
curve_phase.setData(phase_x, phase_y, autoDownsample=False)
else:
curve_phase.setData([], [])
if displayed_phase_high is not None:
phase_high_width = min(xs.size, displayed_phase_high.size)
phase_high_x, phase_high_y = decimate_curve_for_display(
xs[:phase_high_width],
displayed_phase_high[:phase_high_width],
)
phase_high_x, phase_high_y = sanitize_curve_data_for_display(phase_high_x, phase_high_y)
curve_phase_high.setData(phase_high_x, phase_high_y, autoDownsample=False)
else:
curve_phase_high.setData([], [])
if runtime.calib_envelope is not None:
if (not active_do1_tagged) and runtime.calib_envelope is not None:
if runtime.current_sweep_raw is not None:
displayed_calib = resample_envelope(runtime.calib_envelope, runtime.current_sweep_raw.size)
xs_calib = xs[: displayed_calib.size]
@ -2386,7 +2713,7 @@ def run_pyqtgraph(args) -> None:
else:
curve_calib.setData([], [])
if runtime.current_sweep_norm is not None:
if (not active_do1_tagged) and runtime.current_sweep_norm is not None:
norm_display = runtime.current_sweep_norm * norm_display_scale
norm_x, norm_y = decimate_curve_for_display(xs[: norm_display.size], norm_display)
norm_x, norm_y = sanitize_curve_data_for_display(norm_x, norm_y)
@ -2395,7 +2722,13 @@ def run_pyqtgraph(args) -> None:
curve_norm.setData([], [])
if fixed_ylim is None:
if active_signal_kind == "bin_iq":
if active_do1_tagged:
y_series = [
runtime.current_do1_tagged_raw_low,
runtime.current_do1_tagged_raw_high,
runtime.current_sweep_raw,
]
elif active_signal_kind == "bin_iq":
y_series = [
runtime.current_sweep_raw,
displayed_calib,
@ -2416,10 +2749,14 @@ def run_pyqtgraph(args) -> None:
aux_limits = compute_auto_ylim(
displayed_aux[0] if displayed_aux is not None else None,
displayed_aux[1] if displayed_aux is not None else None,
displayed_tagged_aux_low[0] if displayed_tagged_aux_low is not None else None,
displayed_tagged_aux_low[1] if displayed_tagged_aux_low is not None else None,
displayed_tagged_aux_high[0] if displayed_tagged_aux_high is not None else None,
displayed_tagged_aux_high[1] if displayed_tagged_aux_high is not None else None,
)
if aux_limits is not None:
p_line_aux_vb.setYRange(aux_limits[0], aux_limits[1], padding=0)
phase_limits = compute_auto_ylim(displayed_phase)
phase_limits = compute_auto_ylim(displayed_phase, displayed_phase_high)
if phase_limits is not None:
p_line_phase.setYRange(phase_limits[0], phase_limits[1], padding=0)
@ -2457,10 +2794,12 @@ def run_pyqtgraph(args) -> None:
curve_complex_calib_real.setData([], [])
curve_complex_calib_imag.setData([], [])
sweep_for_fft = runtime.current_fft_input
if sweep_for_fft is None:
sweep_for_fft = None if active_do1_tagged else runtime.current_fft_input
if (not active_do1_tagged) and sweep_for_fft is None:
sweep_for_fft = runtime.current_sweep_norm if runtime.current_sweep_norm is not None else runtime.current_sweep_raw
distance_axis = runtime.current_distances if runtime.current_distances is not None else runtime.ring.distance_axis
distance_axis = None if active_do1_tagged else (
runtime.current_distances if runtime.current_distances is not None else runtime.ring.distance_axis
)
if sweep_for_fft is not None and sweep_for_fft.size > 0 and distance_axis is not None:
if (
runtime.current_fft_mag is None
@ -2706,7 +3045,7 @@ def run_pyqtgraph(args) -> None:
refresh_peak_params_label([])
runtime.plot_dirty = False
if changed and runtime.ring.ring is not None:
if changed and runtime.ring.ring is not None and (not do1_tagged_now):
if refresh_heavy_views:
disp = sanitize_image_for_display(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS))
if disp is not None:
@ -2727,6 +3066,8 @@ def run_pyqtgraph(args) -> None:
if redraw_needed or status_dirty:
try:
status_payload = dict(runtime.current_info) if runtime.current_info else {}
if status_payload:
status_payload = {k: v for k, v in status_payload.items() if not str(k).startswith("_")}
if peak_calibrate_mode and runtime.current_peak_width is not None:
status_payload["peak_w"] = runtime.current_peak_width
if peak_calibrate_mode and runtime.current_peak_amplitude is not None:
@ -2771,7 +3112,7 @@ def run_pyqtgraph(args) -> None:
except Exception:
pass
if redraw_needed and runtime.ring.ring_fft is not None:
if redraw_needed and runtime.ring.ring_fft is not None and (not do1_tagged_now):
if not refresh_bscan_views:
log_debug_event(
"suppressed_fft_image_refresh",

View File

@ -5,12 +5,13 @@ from __future__ import annotations
import math
import time
from collections import deque
from typing import List, Optional, Sequence, Set
from typing import Dict, List, Optional, Sequence, Set
import numpy as np
from rfg_adc_plotter.constants import DATA_INVERSION_THRESHOLD, LOG_BASE, LOG_EXP_LIMIT, LOG_POSTSCALER, LOG_SCALER
from rfg_adc_plotter.types import (
Do1Level,
ParserEvent,
PointEvent,
SignalKind,
@ -162,6 +163,14 @@ class LegacyBinaryParser:
self._seen_points = False
self._mode: Optional[str] = None
self._current_signal_kind: Optional[SignalKind] = None
self._last_tagged_step_by_level: Dict[Do1Level, Optional[int]] = {
"low": None,
"high": None,
}
def _reset_tagged_steps(self) -> None:
self._last_tagged_step_by_level["low"] = None
self._last_tagged_step_by_level["high"] = None
@staticmethod
def _u16_at(buf: bytearray, offset: int) -> int:
@ -172,6 +181,7 @@ class LegacyBinaryParser:
self._last_step = None
self._seen_points = False
self._current_signal_kind = None
self._reset_tagged_steps()
events.append(StartEvent(ch=int(ch)))
def _emit_bin_start(self, events: List[ParserEvent], signal_kind: SignalKind) -> None:
@ -179,6 +189,7 @@ class LegacyBinaryParser:
self._last_step = None
self._seen_points = False
self._current_signal_kind = signal_kind
self._reset_tagged_steps()
events.append(StartEvent(ch=0, signal_kind=signal_kind))
def _emit_tty_start(self, events: List[ParserEvent]) -> None:
@ -187,6 +198,7 @@ class LegacyBinaryParser:
def _emit_legacy_point(self, events: List[ParserEvent], step: int, value_word_hi: int, value_word_lo: int, ch: int) -> None:
self._mode = "legacy"
self._current_signal_kind = None
self._reset_tagged_steps()
if self._seen_points and self._last_step is not None and step <= self._last_step:
events.append(StartEvent(ch=int(ch)))
self._seen_points = True
@ -194,7 +206,13 @@ class LegacyBinaryParser:
value = u32_to_i32((int(value_word_hi) << 16) | int(value_word_lo))
events.append(PointEvent(ch=int(ch), x=int(step), y=float(value)))
def _prepare_bin_point(self, events: List[ParserEvent], step: int, signal_kind: SignalKind) -> None:
def _prepare_bin_point(
self,
events: List[ParserEvent],
step: int,
signal_kind: SignalKind,
do1_level: Optional[Do1Level] = None,
) -> None:
self._mode = "bin"
if self._current_signal_kind != signal_kind:
if self._seen_points:
@ -202,12 +220,28 @@ class LegacyBinaryParser:
self._last_step = None
self._seen_points = False
self._current_signal_kind = signal_kind
self._reset_tagged_steps()
if signal_kind == "bin_iq_do1_tagged":
level: Do1Level = "high" if do1_level == "high" else "low"
last_level_step = self._last_tagged_step_by_level[level]
if self._seen_points and last_level_step is not None and step <= last_level_step:
events.append(StartEvent(ch=0, signal_kind=signal_kind))
self._last_step = None
self._seen_points = False
self._reset_tagged_steps()
self._seen_points = True
self._last_tagged_step_by_level[level] = int(step)
self._last_step = int(step)
return
if self._seen_points and self._last_step is not None and step <= self._last_step:
events.append(StartEvent(ch=0, signal_kind=signal_kind))
self._last_step = None
self._seen_points = False
self._seen_points = True
self._last_step = int(step)
self._reset_tagged_steps()
def _emit_tty_point(self, events: List[ParserEvent], step: int, ch_1_word: int, ch_2_word: int) -> None:
self._prepare_bin_point(events, step=int(step), signal_kind="bin_iq")
@ -223,6 +257,33 @@ class LegacyBinaryParser:
)
)
def _emit_tty_tagged_point(
self,
events: List[ParserEvent],
step: int,
ch_1_word: int,
ch_2_word: int,
do1_level: Do1Level,
) -> None:
self._prepare_bin_point(
events,
step=int(step),
signal_kind="bin_iq_do1_tagged",
do1_level=do1_level,
)
ch_1 = u16_to_i16(int(ch_1_word))
ch_2 = u16_to_i16(int(ch_2_word))
events.append(
PointEvent(
ch=0,
x=int(step),
y=tty_ch_pair_to_sweep(ch_1, ch_2),
aux=(float(ch_1), float(ch_2)),
signal_kind="bin_iq_do1_tagged",
do1_level=do1_level,
)
)
def _emit_logdet_point(self, events: List[ParserEvent], step: int, value_word: int) -> None:
self._prepare_bin_point(events, step=int(step), signal_kind="bin_logdet")
value = u16_to_i16(int(value_word))
@ -249,6 +310,8 @@ class LegacyBinaryParser:
is_tty_start = (w0 == 0x000A and w1 == 0xFFFF and w2 == 0xFFFF and w3 == 0xFFFF)
is_legacy_point = (self._buf[6] == 0x0A and w0 != 0xFFFF)
is_tty_point = (w0 == 0x000A and w1 != 0xFFFF)
is_tty_tagged_low_point = (w0 == 0x00A3 and w1 != 0xFFFF)
is_tty_tagged_high_point = (w0 == 0x00A4 and w1 != 0xFFFF)
is_logdet_point = (w0 == 0x001A and w3 == 0x0000)
if is_legacy_start:
@ -281,6 +344,26 @@ class LegacyBinaryParser:
self._emit_tty_point(events, step=int(w1), ch_1_word=int(w2), ch_2_word=int(w3))
del self._buf[:8]
continue
if is_tty_tagged_low_point and (not is_legacy_point):
self._emit_tty_tagged_point(
events,
step=int(w1),
ch_1_word=int(w2),
ch_2_word=int(w3),
do1_level="low",
)
del self._buf[:8]
continue
if is_tty_tagged_high_point and (not is_legacy_point):
self._emit_tty_tagged_point(
events,
step=int(w1),
ch_1_word=int(w2),
ch_2_word=int(w3),
do1_level="high",
)
del self._buf[:8]
continue
del self._buf[:1]
continue
@ -289,6 +372,26 @@ class LegacyBinaryParser:
self._emit_tty_point(events, step=int(w1), ch_1_word=int(w2), ch_2_word=int(w3))
del self._buf[:8]
continue
if is_tty_tagged_low_point:
self._emit_tty_tagged_point(
events,
step=int(w1),
ch_1_word=int(w2),
ch_2_word=int(w3),
do1_level="low",
)
del self._buf[:8]
continue
if is_tty_tagged_high_point:
self._emit_tty_tagged_point(
events,
step=int(w1),
ch_1_word=int(w2),
ch_2_word=int(w3),
do1_level="high",
)
del self._buf[:8]
continue
if is_legacy_point and (not is_tty_point):
self._emit_legacy_point(
events,
@ -309,6 +412,28 @@ class LegacyBinaryParser:
del self._buf[:8]
continue
if is_tty_tagged_low_point and (not is_legacy_point):
self._emit_tty_tagged_point(
events,
step=int(w1),
ch_1_word=int(w2),
ch_2_word=int(w3),
do1_level="low",
)
del self._buf[:8]
continue
if is_tty_tagged_high_point and (not is_legacy_point):
self._emit_tty_tagged_point(
events,
step=int(w1),
ch_1_word=int(w2),
ch_2_word=int(w3),
do1_level="high",
)
del self._buf[:8]
continue
if is_legacy_point and (not is_tty_point):
self._emit_legacy_point(
events,
@ -524,15 +649,34 @@ class SweepAssembler:
self._ys: list[float] = []
self._aux_1: list[float] = []
self._aux_2: list[float] = []
self._tagged_low_xs: list[int] = []
self._tagged_low_ys: list[float] = []
self._tagged_low_aux_1: list[float] = []
self._tagged_low_aux_2: list[float] = []
self._tagged_high_xs: list[int] = []
self._tagged_high_ys: list[float] = []
self._tagged_high_aux_1: list[float] = []
self._tagged_high_aux_2: list[float] = []
self._cur_channel: Optional[int] = None
self._cur_signal_kind: Optional[SignalKind] = None
self._cur_channels: set[int] = set()
def _reset_tagged_current(self) -> None:
self._tagged_low_xs.clear()
self._tagged_low_ys.clear()
self._tagged_low_aux_1.clear()
self._tagged_low_aux_2.clear()
self._tagged_high_xs.clear()
self._tagged_high_ys.clear()
self._tagged_high_aux_1.clear()
self._tagged_high_aux_2.clear()
def _reset_current(self) -> None:
self._xs.clear()
self._ys.clear()
self._aux_1.clear()
self._aux_2.clear()
self._reset_tagged_current()
self._cur_channel = None
self._cur_signal_kind = None
self._cur_channels.clear()
@ -567,6 +711,24 @@ class SweepAssembler:
if last_idx < series.size - 1:
series[last_idx + 1 :] = series[last_idx]
@staticmethod
def _nanmean_pair(primary: np.ndarray, secondary: np.ndarray) -> np.ndarray:
width = min(primary.size, secondary.size)
if width <= 0:
return np.zeros((0,), dtype=np.float32)
first = np.asarray(primary[:width], dtype=np.float32)
second = np.asarray(secondary[:width], dtype=np.float32)
out = np.full((width,), np.nan, dtype=np.float32)
first_valid = np.isfinite(first)
second_valid = np.isfinite(second)
both_valid = first_valid & second_valid
only_first = first_valid & (~second_valid)
only_second = second_valid & (~first_valid)
out[only_first] = first[only_first]
out[only_second] = second[only_second]
out[both_valid] = (first[both_valid] + second[both_valid]) * 0.5
return out
def consume(self, event: ParserEvent) -> Optional[SweepPacket]:
if isinstance(event, StartEvent):
packet = self.finalize_current()
@ -598,7 +760,21 @@ class SweepAssembler:
self._cur_channels.add(point_ch)
self._xs.append(int(event.x))
self._ys.append(float(event.y))
if event.aux is not None:
if self._cur_signal_kind == "bin_iq_do1_tagged":
level = "high" if event.do1_level == "high" else "low"
if level == "low":
self._tagged_low_xs.append(int(event.x))
self._tagged_low_ys.append(float(event.y))
if event.aux is not None:
self._tagged_low_aux_1.append(float(event.aux[0]))
self._tagged_low_aux_2.append(float(event.aux[1]))
else:
self._tagged_high_xs.append(int(event.x))
self._tagged_high_ys.append(float(event.y))
if event.aux is not None:
self._tagged_high_aux_1.append(float(event.aux[0]))
self._tagged_high_aux_2.append(float(event.aux[1]))
elif event.aux is not None:
self._aux_1.append(float(event.aux[0]))
self._aux_2.append(float(event.aux[1]))
return packet
@ -613,13 +789,37 @@ class SweepAssembler:
self._max_width = max(self._max_width, width)
target_width = self._max_width if self._fancy else width
sweep = self._scatter(self._xs, self._ys, target_width)
aux_curves: SweepAuxCurves = None
if self._aux_1 and self._aux_2 and len(self._aux_1) == len(self._xs):
aux_curves = (
self._scatter(self._xs, self._aux_1, target_width),
self._scatter(self._xs, self._aux_2, target_width),
)
do1_tagged_payload = None
if self._cur_signal_kind == "bin_iq_do1_tagged":
raw_low = self._scatter(self._tagged_low_xs, self._tagged_low_ys, target_width)
raw_high = self._scatter(self._tagged_high_xs, self._tagged_high_ys, target_width)
sweep = self._nanmean_pair(raw_low, raw_high)
aux_low = None
if self._tagged_low_aux_1 and self._tagged_low_aux_2 and len(self._tagged_low_aux_1) == len(self._tagged_low_xs):
aux_low = (
self._scatter(self._tagged_low_xs, self._tagged_low_aux_1, target_width),
self._scatter(self._tagged_low_xs, self._tagged_low_aux_2, target_width),
)
aux_high = None
if self._tagged_high_aux_1 and self._tagged_high_aux_2 and len(self._tagged_high_aux_1) == len(self._tagged_high_xs):
aux_high = (
self._scatter(self._tagged_high_xs, self._tagged_high_aux_1, target_width),
self._scatter(self._tagged_high_xs, self._tagged_high_aux_2, target_width),
)
do1_tagged_payload = {
"raw_low": raw_low,
"raw_high": raw_high,
"aux_low": aux_low,
"aux_high": aux_high,
}
else:
sweep = self._scatter(self._xs, self._ys, target_width)
if self._aux_1 and self._aux_2 and len(self._aux_1) == len(self._xs):
aux_curves = (
self._scatter(self._xs, self._aux_1, target_width),
self._scatter(self._xs, self._aux_2, target_width),
)
n_valid_cur = int(np.count_nonzero(np.isfinite(sweep)))
@ -670,4 +870,6 @@ class SweepAssembler:
"std": std,
"dt_ms": dt_ms,
}
if do1_tagged_payload is not None:
info["_do1_tagged_payload"] = do1_tagged_payload
return (sweep, info, aux_curves)

View File

@ -50,7 +50,7 @@ def _looks_like_legacy_8byte_stream(data: bytes) -> bool:
w0 = _u16le_at(buf, base)
w1 = _u16le_at(buf, base + 2)
w3 = _u16le_at(buf, base + 6)
if w0 == 0x000A and w1 != 0xFFFF:
if w0 in {0x000A, 0x00A3, 0x00A4} and w1 != 0xFFFF:
matched_steps_tty.append(w1)
elif w0 == 0x001A and w3 == 0x0000:
matched_steps_logdet.append(w1)
@ -232,7 +232,8 @@ class SweepReader(threading.Thread):
)
)
sys.stderr.write(
"[hint] parser_16_bit_x2: if source is 8-byte tty CH1/CH2 stream (0x000A,step,ch1,ch2), try --bin\n"
"[hint] parser_16_bit_x2: if source is 8-byte tty CH1/CH2 stream "
"(0x000A/0x00A3/0x00A4,step,ch1,ch2), try --bin\n"
)
assembler = SweepAssembler(fancy=self._fancy, apply_inversion=False)
return parser, assembler, []
@ -358,7 +359,8 @@ class SweepReader(threading.Thread):
and (now_s - self._started_at) >= _NO_PACKET_HINT_AFTER_S
):
sys.stderr.write(
"[hint] parser_16_bit_x2 still has no sweeps; if source is tty CH1/CH2, rerun with --bin\n"
"[hint] parser_16_bit_x2 still has no sweeps; "
"if source is tty CH1/CH2 (0x000A/0x00A3/0x00A4), rerun with --bin\n"
)
parser_hint_emitted = True
time.sleep(0.0005)

View File

@ -24,6 +24,12 @@ class RuntimeState:
full_current_fft_source: Optional[np.ndarray] = None
full_current_aux_curves: SweepAuxCurves = None
full_current_aux_curves_codes: SweepAuxCurves = None
full_do1_tagged_raw_low: Optional[np.ndarray] = None
full_do1_tagged_raw_high: Optional[np.ndarray] = None
full_do1_tagged_aux_low: SweepAuxCurves = None
full_do1_tagged_aux_high: SweepAuxCurves = None
full_do1_tagged_aux_low_codes: SweepAuxCurves = None
full_do1_tagged_aux_high_codes: SweepAuxCurves = None
current_freqs: Optional[np.ndarray] = None
current_distances: Optional[np.ndarray] = None
current_sweep_raw: Optional[np.ndarray] = None
@ -31,6 +37,10 @@ class RuntimeState:
current_fft_input: Optional[np.ndarray] = None
current_fft_complex: Optional[np.ndarray] = None
current_aux_curves: SweepAuxCurves = None
current_do1_tagged_raw_low: Optional[np.ndarray] = None
current_do1_tagged_raw_high: Optional[np.ndarray] = None
current_do1_tagged_aux_low: SweepAuxCurves = None
current_do1_tagged_aux_high: SweepAuxCurves = None
current_sweep_norm: Optional[np.ndarray] = None
current_fft_mag: Optional[np.ndarray] = None
current_fft_db: Optional[np.ndarray] = None

View File

@ -9,7 +9,8 @@ import numpy as np
Number = Union[int, float]
SignalKind = Literal["bin_iq", "bin_logdet"]
SignalKind = Literal["bin_iq", "bin_logdet", "bin_iq_do1_tagged"]
Do1Level = Literal["low", "high"]
SweepInfo = Dict[str, Any]
SweepData = Dict[str, np.ndarray]
SweepAuxCurves = Optional[Tuple[np.ndarray, np.ndarray]]
@ -29,6 +30,7 @@ class PointEvent:
y: float
aux: Optional[Tuple[float, float]] = None
signal_kind: Optional[SignalKind] = None
do1_level: Optional[Do1Level] = None
ParserEvent: TypeAlias = Union[StartEvent, PointEvent]