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

@ -111,9 +111,11 @@ Legacy binary:
`--bin` понимает mixed 8-байтный поток: `--bin` понимает mixed 8-байтный поток:
- `0x000A,step,ch1_i16,ch2_i16` для CH1/CH2 из `kamil_adc` (`tty:/tmp/ttyADC_data`) - `0x000A,step,ch1_i16,ch2_i16` для CH1/CH2 из `kamil_adc` (`tty:/tmp/ttyADC_data`)
- `0x00A3,step,ch1_i16,ch2_i16` и `0x00A4,step,ch1_i16,ch2_i16` для DO1 LOW/HIGH tagged fast-tty
- `0x001A,step,data_i16,0x0000` для логарифмического детектора - `0x001A,step,data_i16,0x0000` для логарифмического детектора
Для `0x000A` сырая кривая строится как `ch1^2 + ch2^2`, а FFT рассчитывается от комплексного сигнала `ch1 + i*ch2`. Для `0x000A` сырая кривая строится как `ch1^2 + ch2^2`, а FFT рассчитывается от комплексного сигнала `ch1 + i*ch2`.
Для `0x00A3/0x00A4` tagged-режим определяется автоматически: LOW/HIGH отображаются раздельно в raw/aux/phase, а waterfall/FFT/B-scan скрываются.
Для `0x001A` signed `data_i16` сначала переводится в В, затем raw отображается как `V`, а FFT рассчитывается от `exp(V)`. Для `0x001A` signed `data_i16` сначала переводится в В, затем raw отображается как `V`, а FFT рассчитывается от `exp(V)`.
Параметр `--tty-range-v` применяется к обоим типам `--bin`-данных. Параметр `--tty-range-v` применяется к обоим типам `--bin`-данных.

View File

@ -73,9 +73,12 @@ def build_parser() -> argparse.ArgumentParser:
help=( help=(
"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, "
"либо 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 переводятся в В, " "Для 0x000A: после парсинга int16 переводятся в В, "
"сырая кривая = ch1^2+ch2^2 (В^2), FFT вход = ch1+i*ch2 (В). " "сырая кривая = 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)" "Для 0x001A: code_i16 переводится в В, raw = V, FFT вход = exp(V)"
), ),
) )

View File

@ -8,7 +8,7 @@ import sys
import threading import threading
import time import time
from queue import Empty, Queue 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 import numpy as np
@ -67,6 +67,7 @@ TTY_RANGE_DEFAULT_V = 5.0
TTY_RANGE_MIN_V = 1e-6 TTY_RANGE_MIN_V = 1e-6
TTY_RANGE_MAX_V = 1_000_000.0 TTY_RANGE_MAX_V = 1_000_000.0
LOGDET_EXP_INPUT_LIMIT = 80.0 LOGDET_EXP_INPUT_LIMIT = 80.0
DO1_TAGGED_INFO_KEY = "_do1_tagged_payload"
def sanitize_curve_data_for_display( 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 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]: 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.""" """Compute phase-like curve atan2(aux_2, aux_1) for raw CH2/CH1 display."""
if aux_curves is None: if aux_curves is None:
@ -508,6 +553,17 @@ def compute_aux_phase_curve(aux_curves: SweepAuxCurves) -> Optional[np.ndarray]:
return phase 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: 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.""" """Return a finite positive full-scale voltage range for tty int16 conversion."""
try: try:
@ -810,6 +866,8 @@ def run_pyqtgraph(args) -> None:
p_line = win.addPlot(row=0, col=0, title="Сырые данные") p_line = win.addPlot(row=0, col=0, title="Сырые данные")
p_line.showGrid(x=True, y=True, alpha=0.3) p_line.showGrid(x=True, y=True, alpha=0.3)
curve = p_line.plot(pen=pg.mkPen((80, 120, 255), width=1)) 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 p_line_aux_vb = None
if bin_iq_power_mode: if bin_iq_power_mode:
p_line_aux_vb = pg.ViewBox() p_line_aux_vb = pg.ViewBox()
@ -823,15 +881,23 @@ def run_pyqtgraph(args) -> None:
p_line_aux_vb = None p_line_aux_vb = None
curve_aux_1 = pg.PlotDataItem(pen=pg.mkPen((255, 170, 40), width=1)) 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_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: if p_line_aux_vb is not None:
p_line_aux_vb.addItem(curve_aux_1) p_line_aux_vb.addItem(curve_aux_1)
p_line_aux_vb.addItem(curve_aux_2) 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: else:
p_line.addItem(curve_aux_1) p_line.addItem(curve_aux_1)
p_line.addItem(curve_aux_2) p_line.addItem(curve_aux_2)
p_line.addItem(curve_aux_3)
p_line.addItem(curve_aux_4)
else: else:
curve_aux_1 = p_line.plot(pen=pg.mkPen((255, 170, 40), width=1)) 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_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_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)) curve_norm = p_line.plot(pen=pg.mkPen((60, 180, 90), width=1))
p_line.setLabel("bottom", "ГГц") 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 = win.addPlot(row=2, col=0, title="Raw phase: atan(CH2/CH1)")
p_line_phase.showGrid(x=True, y=True, alpha=0.3) 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 = 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("bottom", "ГГц")
p_line_phase.setLabel("left", "рад") p_line_phase.setLabel("left", "рад")
try: try:
@ -1312,18 +1379,23 @@ def run_pyqtgraph(args) -> None:
if not isinstance(payload, dict): if not isinstance(payload, dict):
return None return None
signal_kind = payload.get("signal_kind") 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 str(signal_kind)
return None 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: 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: def refresh_signal_mode_labels() -> None:
signal_kind = get_signal_kind() signal_kind = get_signal_kind()
active_complex = current_packet_is_complex() active_complex = current_packet_is_complex()
is_logdet = signal_kind == "bin_logdet" is_logdet = signal_kind == "bin_logdet"
is_bin_iq = signal_kind == "bin_iq" is_bin_iq = signal_kind == "bin_iq"
is_do1_tagged = signal_kind == "bin_iq_do1_tagged"
try: try:
if is_logdet: if is_logdet:
@ -1331,6 +1403,11 @@ def run_pyqtgraph(args) -> None:
p_line.setLabel("left", "В") p_line.setLabel("left", "В")
p_fft.setTitle("FFT: exp(V)") p_fft.setTitle("FFT: exp(V)")
parsed_data_cb.setText("Сырые log-detector (В)") 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: elif is_bin_iq:
p_line.setTitle("Сырые CH1/CH2 (В) и CH1^2 + CH2^2 (В^2)") p_line.setTitle("Сырые CH1/CH2 (В) и CH1^2 + CH2^2 (В^2)")
p_line.setLabel("left", "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") p_fft.setTitle("FFT")
parsed_data_cb.setText("данные после парсинга") parsed_data_cb.setText("данные после парсинга")
p_fft.setLabel("left", "Амплитуда" if active_complex else "дБ") 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: except Exception:
pass pass
@ -1365,6 +1445,12 @@ def run_pyqtgraph(args) -> None:
return False return False
ch_1_v = convert_tty_i16_to_voltage(code_1_arr[:width], tty_range_v) 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) 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_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)) 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_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 runtime.full_current_sweep_codes = None
return True 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: def rebuild_logdet_voltage_curve_from_codes() -> bool:
if (not bin_iq_power_mode) or runtime.full_current_sweep_codes is None: if (not bin_iq_power_mode) or runtime.full_current_sweep_codes is None:
return False return False
@ -1380,6 +1504,12 @@ def run_pyqtgraph(args) -> None:
if code_arr.size <= 0: if code_arr.size <= 0:
return False return False
sweep_raw_v, fft_input = build_logdet_voltage_fft_input(code_arr, tty_range_v) 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 = None
runtime.full_current_aux_curves_codes = None runtime.full_current_aux_curves_codes = None
runtime.full_current_sweep_raw = sweep_raw_v runtime.full_current_sweep_raw = sweep_raw_v
@ -1390,6 +1520,8 @@ def run_pyqtgraph(args) -> None:
signal_kind = get_signal_kind() signal_kind = get_signal_kind()
if signal_kind == "bin_logdet": if signal_kind == "bin_logdet":
return rebuild_logdet_voltage_curve_from_codes() 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() return rebuild_tty_voltage_curves_from_codes()
def reset_background_state(*, clear_profile: bool = True) -> None: def reset_background_state(*, clear_profile: bool = True) -> None:
@ -1427,6 +1559,7 @@ def run_pyqtgraph(args) -> None:
if reset_ring: if reset_ring:
reset_ring_buffers() 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: if runtime.full_current_freqs is None or runtime.full_current_sweep_raw is None:
runtime.current_freqs = None runtime.current_freqs = None
runtime.current_sweep_raw = None runtime.current_sweep_raw = None
@ -1434,6 +1567,10 @@ def run_pyqtgraph(args) -> None:
runtime.current_fft_input = None runtime.current_fft_input = None
runtime.current_fft_complex = None runtime.current_fft_complex = None
runtime.current_aux_curves = 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_sweep_norm = None
runtime.current_fft_mag = None runtime.current_fft_mag = None
runtime.current_fft_db = None runtime.current_fft_db = None
@ -1462,6 +1599,40 @@ def run_pyqtgraph(args) -> None:
runtime.range_min_ghz, runtime.range_min_ghz,
runtime.range_max_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 runtime.current_sweep_raw.size == 0:
if push_to_ring: if push_to_ring:
@ -1472,6 +1643,10 @@ def run_pyqtgraph(args) -> None:
runtime.current_fft_input = None runtime.current_fft_input = None
runtime.current_fft_complex = None runtime.current_fft_complex = None
runtime.current_aux_curves = 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_sweep_norm = None
runtime.current_fft_mag = None runtime.current_fft_mag = None
runtime.current_fft_db = 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) recompute_current_processed_sweep(push_to_ring=push_to_ring)
def recompute_current_processed_sweep(push_to_ring: bool = False) -> None: 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 fft_source = runtime.current_fft_source
if fft_source is None and runtime.current_sweep_raw is not None: if fft_source is None and runtime.current_sweep_raw is not None:
fft_source = np.asarray(runtime.current_sweep_raw, dtype=np.float32) 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_aux_curves_codes = None
runtime.full_current_sweep_codes = None runtime.full_current_sweep_codes = None
runtime.full_current_fft_source = 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) 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( calibrated = calibrate_freqs(
{ {
"F": base_freqs, "F": base_freqs,
@ -2242,7 +2495,7 @@ def run_pyqtgraph(args) -> None:
runtime.full_current_aux_curves_codes = 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 (signal_kind != "bin_iq_do1_tagged") and runtime.full_current_fft_source is None:
calibrated = calibrate_freqs( calibrated = calibrate_freqs(
{ {
"F": base_freqs, "F": base_freqs,
@ -2327,11 +2580,13 @@ def run_pyqtgraph(args) -> None:
or last_bscan_refresh_stride <= 1 or last_bscan_refresh_stride <= 1
or (update_ticks % last_bscan_refresh_stride) == 0 or (update_ticks % last_bscan_refresh_stride) == 0
) )
do1_tagged_now = current_packet_is_do1_tagged()
if redraw_needed: if redraw_needed:
refresh_signal_mode_labels() refresh_signal_mode_labels()
active_signal_kind = get_signal_kind() active_signal_kind = get_signal_kind()
active_complex_mode = current_packet_is_complex() active_complex_mode = current_packet_is_complex()
active_do1_tagged = active_signal_kind == "bin_iq_do1_tagged"
xs = resolve_curve_xs( xs = resolve_curve_xs(
runtime.current_sweep_raw.size runtime.current_sweep_raw.size
if runtime.current_sweep_raw is not None if runtime.current_sweep_raw is not None
@ -2339,20 +2594,78 @@ def run_pyqtgraph(args) -> None:
) )
displayed_calib = None displayed_calib = None
displayed_aux = resolve_visible_aux_curves(runtime.current_aux_curves, parsed_data_enabled) 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: 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: except Exception:
pass 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 = decimate_curve_for_display(xs, runtime.current_sweep_raw)
raw_x, raw_y = sanitize_curve_data_for_display(raw_x, raw_y) raw_x, raw_y = sanitize_curve_data_for_display(raw_x, raw_y)
curve.setData(raw_x, raw_y, autoDownsample=False) curve.setData(raw_x, raw_y, autoDownsample=False)
curve_raw_low.setData([], [])
curve_raw_high.setData([], [])
else: else:
curve.setData([], []) 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_1, aux_2 = displayed_aux
aux_width = min(xs.size, aux_1.size, aux_2.size) 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]) 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) 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_1.setData(aux_x_1, aux_y_1, autoDownsample=False)
curve_aux_2.setData(aux_x_2, aux_y_2, autoDownsample=False) curve_aux_2.setData(aux_x_2, aux_y_2, autoDownsample=False)
curve_aux_3.setData([], [])
curve_aux_4.setData([], [])
else: else:
curve_aux_1.setData([], []) curve_aux_1.setData([], [])
curve_aux_2.setData([], []) curve_aux_2.setData([], [])
curve_aux_3.setData([], [])
curve_aux_4.setData([], [])
if displayed_phase is not None: if displayed_phase is not None:
phase_width = min(xs.size, displayed_phase.size) 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) curve_phase.setData(phase_x, phase_y, autoDownsample=False)
else: else:
curve_phase.setData([], []) 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: if runtime.current_sweep_raw is not None:
displayed_calib = resample_envelope(runtime.calib_envelope, runtime.current_sweep_raw.size) displayed_calib = resample_envelope(runtime.calib_envelope, runtime.current_sweep_raw.size)
xs_calib = xs[: displayed_calib.size] xs_calib = xs[: displayed_calib.size]
@ -2386,7 +2713,7 @@ def run_pyqtgraph(args) -> None:
else: else:
curve_calib.setData([], []) 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_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 = decimate_curve_for_display(xs[: norm_display.size], norm_display)
norm_x, norm_y = sanitize_curve_data_for_display(norm_x, norm_y) 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([], []) curve_norm.setData([], [])
if fixed_ylim is None: 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 = [ y_series = [
runtime.current_sweep_raw, runtime.current_sweep_raw,
displayed_calib, displayed_calib,
@ -2416,10 +2749,14 @@ def run_pyqtgraph(args) -> None:
aux_limits = compute_auto_ylim( aux_limits = compute_auto_ylim(
displayed_aux[0] if displayed_aux is not None else None, displayed_aux[0] if displayed_aux is not None else None,
displayed_aux[1] 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: if aux_limits is not None:
p_line_aux_vb.setYRange(aux_limits[0], aux_limits[1], padding=0) 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: if phase_limits is not None:
p_line_phase.setYRange(phase_limits[0], phase_limits[1], padding=0) 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_real.setData([], [])
curve_complex_calib_imag.setData([], []) curve_complex_calib_imag.setData([], [])
sweep_for_fft = runtime.current_fft_input sweep_for_fft = None if active_do1_tagged else runtime.current_fft_input
if sweep_for_fft is None: 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 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 sweep_for_fft is not None and sweep_for_fft.size > 0 and distance_axis is not None:
if ( if (
runtime.current_fft_mag is None runtime.current_fft_mag is None
@ -2706,7 +3045,7 @@ def run_pyqtgraph(args) -> None:
refresh_peak_params_label([]) refresh_peak_params_label([])
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 and (not do1_tagged_now):
if refresh_heavy_views: if refresh_heavy_views:
disp = sanitize_image_for_display(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS)) disp = sanitize_image_for_display(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS))
if disp is not None: if disp is not None:
@ -2727,6 +3066,8 @@ def run_pyqtgraph(args) -> None:
if redraw_needed or status_dirty: if redraw_needed or status_dirty:
try: try:
status_payload = dict(runtime.current_info) if runtime.current_info else {} 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: if peak_calibrate_mode and runtime.current_peak_width is not None:
status_payload["peak_w"] = runtime.current_peak_width status_payload["peak_w"] = runtime.current_peak_width
if peak_calibrate_mode and runtime.current_peak_amplitude is not None: if peak_calibrate_mode and runtime.current_peak_amplitude is not None:
@ -2771,7 +3112,7 @@ def run_pyqtgraph(args) -> None:
except Exception: except Exception:
pass 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: if not refresh_bscan_views:
log_debug_event( log_debug_event(
"suppressed_fft_image_refresh", "suppressed_fft_image_refresh",

View File

@ -5,12 +5,13 @@ from __future__ import annotations
import math import math
import time import time
from collections import deque from collections import deque
from typing import List, Optional, Sequence, Set from typing import Dict, List, Optional, Sequence, Set
import numpy as np 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.constants import DATA_INVERSION_THRESHOLD, LOG_BASE, LOG_EXP_LIMIT, LOG_POSTSCALER, LOG_SCALER
from rfg_adc_plotter.types import ( from rfg_adc_plotter.types import (
Do1Level,
ParserEvent, ParserEvent,
PointEvent, PointEvent,
SignalKind, SignalKind,
@ -162,6 +163,14 @@ class LegacyBinaryParser:
self._seen_points = False self._seen_points = False
self._mode: Optional[str] = None self._mode: Optional[str] = None
self._current_signal_kind: Optional[SignalKind] = 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 @staticmethod
def _u16_at(buf: bytearray, offset: int) -> int: def _u16_at(buf: bytearray, offset: int) -> int:
@ -172,6 +181,7 @@ class LegacyBinaryParser:
self._last_step = None self._last_step = None
self._seen_points = False self._seen_points = False
self._current_signal_kind = None self._current_signal_kind = None
self._reset_tagged_steps()
events.append(StartEvent(ch=int(ch))) events.append(StartEvent(ch=int(ch)))
def _emit_bin_start(self, events: List[ParserEvent], signal_kind: SignalKind) -> None: def _emit_bin_start(self, events: List[ParserEvent], signal_kind: SignalKind) -> None:
@ -179,6 +189,7 @@ class LegacyBinaryParser:
self._last_step = None self._last_step = None
self._seen_points = False self._seen_points = False
self._current_signal_kind = signal_kind self._current_signal_kind = signal_kind
self._reset_tagged_steps()
events.append(StartEvent(ch=0, signal_kind=signal_kind)) events.append(StartEvent(ch=0, signal_kind=signal_kind))
def _emit_tty_start(self, events: List[ParserEvent]) -> None: 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: 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._mode = "legacy"
self._current_signal_kind = None 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: if self._seen_points and self._last_step is not None and step <= self._last_step:
events.append(StartEvent(ch=int(ch))) events.append(StartEvent(ch=int(ch)))
self._seen_points = True self._seen_points = True
@ -194,7 +206,13 @@ class LegacyBinaryParser:
value = u32_to_i32((int(value_word_hi) << 16) | int(value_word_lo)) 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))) 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" self._mode = "bin"
if self._current_signal_kind != signal_kind: if self._current_signal_kind != signal_kind:
if self._seen_points: if self._seen_points:
@ -202,12 +220,28 @@ class LegacyBinaryParser:
self._last_step = None self._last_step = None
self._seen_points = False self._seen_points = False
self._current_signal_kind = signal_kind 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: 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)) events.append(StartEvent(ch=0, signal_kind=signal_kind))
self._last_step = None self._last_step = None
self._seen_points = False self._seen_points = False
self._seen_points = True self._seen_points = True
self._last_step = int(step) 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: 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") 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: 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") self._prepare_bin_point(events, step=int(step), signal_kind="bin_logdet")
value = u16_to_i16(int(value_word)) 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_tty_start = (w0 == 0x000A and w1 == 0xFFFF and w2 == 0xFFFF and w3 == 0xFFFF)
is_legacy_point = (self._buf[6] == 0x0A and w0 != 0xFFFF) is_legacy_point = (self._buf[6] == 0x0A and w0 != 0xFFFF)
is_tty_point = (w0 == 0x000A and w1 != 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) is_logdet_point = (w0 == 0x001A and w3 == 0x0000)
if is_legacy_start: 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)) self._emit_tty_point(events, step=int(w1), ch_1_word=int(w2), ch_2_word=int(w3))
del self._buf[:8] del self._buf[:8]
continue 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] del self._buf[:1]
continue 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)) self._emit_tty_point(events, step=int(w1), ch_1_word=int(w2), ch_2_word=int(w3))
del self._buf[:8] del self._buf[:8]
continue 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): if is_legacy_point and (not is_tty_point):
self._emit_legacy_point( self._emit_legacy_point(
events, events,
@ -309,6 +412,28 @@ class LegacyBinaryParser:
del self._buf[:8] del self._buf[:8]
continue 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): if is_legacy_point and (not is_tty_point):
self._emit_legacy_point( self._emit_legacy_point(
events, events,
@ -524,15 +649,34 @@ class SweepAssembler:
self._ys: list[float] = [] self._ys: list[float] = []
self._aux_1: list[float] = [] self._aux_1: list[float] = []
self._aux_2: 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_channel: Optional[int] = None
self._cur_signal_kind: Optional[SignalKind] = None self._cur_signal_kind: Optional[SignalKind] = None
self._cur_channels: set[int] = set() 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: def _reset_current(self) -> None:
self._xs.clear() self._xs.clear()
self._ys.clear() self._ys.clear()
self._aux_1.clear() self._aux_1.clear()
self._aux_2.clear() self._aux_2.clear()
self._reset_tagged_current()
self._cur_channel = None self._cur_channel = None
self._cur_signal_kind = None self._cur_signal_kind = None
self._cur_channels.clear() self._cur_channels.clear()
@ -567,6 +711,24 @@ class SweepAssembler:
if last_idx < series.size - 1: if last_idx < series.size - 1:
series[last_idx + 1 :] = series[last_idx] 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]: def consume(self, event: ParserEvent) -> Optional[SweepPacket]:
if isinstance(event, StartEvent): if isinstance(event, StartEvent):
packet = self.finalize_current() packet = self.finalize_current()
@ -598,7 +760,21 @@ class SweepAssembler:
self._cur_channels.add(point_ch) self._cur_channels.add(point_ch)
self._xs.append(int(event.x)) self._xs.append(int(event.x))
self._ys.append(float(event.y)) 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_1.append(float(event.aux[0]))
self._aux_2.append(float(event.aux[1])) self._aux_2.append(float(event.aux[1]))
return packet return packet
@ -613,13 +789,37 @@ class SweepAssembler:
self._max_width = max(self._max_width, width) self._max_width = max(self._max_width, width)
target_width = self._max_width if self._fancy else width target_width = self._max_width if self._fancy else width
sweep = self._scatter(self._xs, self._ys, target_width)
aux_curves: SweepAuxCurves = None aux_curves: SweepAuxCurves = None
if self._aux_1 and self._aux_2 and len(self._aux_1) == len(self._xs): do1_tagged_payload = None
aux_curves = ( if self._cur_signal_kind == "bin_iq_do1_tagged":
self._scatter(self._xs, self._aux_1, target_width), raw_low = self._scatter(self._tagged_low_xs, self._tagged_low_ys, target_width)
self._scatter(self._xs, self._aux_2, 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))) n_valid_cur = int(np.count_nonzero(np.isfinite(sweep)))
@ -670,4 +870,6 @@ class SweepAssembler:
"std": std, "std": std,
"dt_ms": dt_ms, "dt_ms": dt_ms,
} }
if do1_tagged_payload is not None:
info["_do1_tagged_payload"] = do1_tagged_payload
return (sweep, info, aux_curves) 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) w0 = _u16le_at(buf, base)
w1 = _u16le_at(buf, base + 2) w1 = _u16le_at(buf, base + 2)
w3 = _u16le_at(buf, base + 6) 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) matched_steps_tty.append(w1)
elif w0 == 0x001A and w3 == 0x0000: elif w0 == 0x001A and w3 == 0x0000:
matched_steps_logdet.append(w1) matched_steps_logdet.append(w1)
@ -232,7 +232,8 @@ class SweepReader(threading.Thread):
) )
) )
sys.stderr.write( 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) assembler = SweepAssembler(fancy=self._fancy, apply_inversion=False)
return parser, assembler, [] return parser, assembler, []
@ -358,7 +359,8 @@ class SweepReader(threading.Thread):
and (now_s - self._started_at) >= _NO_PACKET_HINT_AFTER_S and (now_s - self._started_at) >= _NO_PACKET_HINT_AFTER_S
): ):
sys.stderr.write( 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 parser_hint_emitted = True
time.sleep(0.0005) time.sleep(0.0005)

View File

@ -24,6 +24,12 @@ class RuntimeState:
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 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_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
@ -31,6 +37,10 @@ class RuntimeState:
current_fft_input: Optional[np.ndarray] = None current_fft_input: Optional[np.ndarray] = None
current_fft_complex: Optional[np.ndarray] = None current_fft_complex: Optional[np.ndarray] = None
current_aux_curves: SweepAuxCurves = 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_sweep_norm: Optional[np.ndarray] = None
current_fft_mag: Optional[np.ndarray] = None current_fft_mag: Optional[np.ndarray] = None
current_fft_db: 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] 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] SweepInfo = Dict[str, Any]
SweepData = Dict[str, np.ndarray] SweepData = Dict[str, np.ndarray]
SweepAuxCurves = Optional[Tuple[np.ndarray, np.ndarray]] SweepAuxCurves = Optional[Tuple[np.ndarray, np.ndarray]]
@ -29,6 +30,7 @@ class PointEvent:
y: float y: float
aux: Optional[Tuple[float, float]] = None aux: Optional[Tuple[float, float]] = None
signal_kind: Optional[SignalKind] = None signal_kind: Optional[SignalKind] = None
do1_level: Optional[Do1Level] = None
ParserEvent: TypeAlias = Union[StartEvent, PointEvent] ParserEvent: TypeAlias = Union[StartEvent, PointEvent]

View File

@ -46,6 +46,7 @@ class CliTests(unittest.TestCase):
self.assertIn("--parser_16_bit_x2", proc.stdout) self.assertIn("--parser_16_bit_x2", proc.stdout)
self.assertIn("--parser_complex_ascii", proc.stdout) self.assertIn("--parser_complex_ascii", proc.stdout)
self.assertIn("--opengl", proc.stdout) self.assertIn("--opengl", proc.stdout)
self.assertIn("0x00A3/0x00A4", proc.stdout)
def test_backend_mpl_reports_removal(self): def test_backend_mpl_reports_removal(self):
proc = _run("-m", "rfg_adc_plotter.main", "/dev/null", "--backend", "mpl") proc = _run("-m", "rfg_adc_plotter.main", "/dev/null", "--backend", "mpl")

View File

@ -15,6 +15,8 @@ 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,
compute_do1_tagged_aggregate,
compute_do1_tagged_phase_curves,
convert_tty_i16_to_voltage, convert_tty_i16_to_voltage,
decimate_bscan_rows_for_display, decimate_bscan_rows_for_display,
decimate_curve_for_display, decimate_curve_for_display,
@ -30,6 +32,7 @@ from rfg_adc_plotter.gui.pyqtgraph_backend import (
set_image_rect_if_ready, set_image_rect_if_ready,
resolve_visible_fft_curves, resolve_visible_fft_curves,
resolve_visible_aux_curves, resolve_visible_aux_curves,
resolve_visible_do1_tagged_aux_curves,
) )
from rfg_adc_plotter.processing.calibration import ( from rfg_adc_plotter.processing.calibration import (
build_calib_envelope, build_calib_envelope,
@ -316,6 +319,53 @@ class ProcessingTests(unittest.TestCase):
self.assertEqual(phase.shape, expected.shape) self.assertEqual(phase.shape, expected.shape)
self.assertTrue(np.allclose(phase, expected, atol=1e-6)) self.assertTrue(np.allclose(phase, expected, atol=1e-6))
def test_compute_do1_tagged_aggregate_nanmean_merges_low_and_high(self):
low = np.asarray([1.0, np.nan, 5.0, np.nan], dtype=np.float32)
high = np.asarray([3.0, 7.0, np.nan, np.nan], dtype=np.float32)
merged = compute_do1_tagged_aggregate(low, high)
self.assertIsNotNone(merged)
self.assertTrue(np.allclose(merged[:3], np.asarray([2.0, 7.0, 5.0], dtype=np.float32), equal_nan=True))
self.assertTrue(np.isnan(merged[3]))
def test_resolve_visible_do1_tagged_aux_curves_obeys_checkbox_state(self):
aux_low = (
np.asarray([1.0, 2.0], dtype=np.float32),
np.asarray([3.0, 4.0], dtype=np.float32),
)
aux_high = (
np.asarray([5.0, 6.0], dtype=np.float32),
np.asarray([7.0, 8.0], dtype=np.float32),
)
hidden_low, hidden_high = resolve_visible_do1_tagged_aux_curves(aux_low, aux_high, enabled=False)
self.assertIsNone(hidden_low)
self.assertIsNone(hidden_high)
visible_low, visible_high = resolve_visible_do1_tagged_aux_curves(aux_low, aux_high, enabled=True)
self.assertIsNotNone(visible_low)
self.assertIsNotNone(visible_high)
self.assertTrue(np.allclose(visible_low[0], aux_low[0]))
self.assertTrue(np.allclose(visible_high[1], aux_high[1]))
def test_compute_do1_tagged_phase_curves_returns_two_independent_series(self):
aux_low = (
np.asarray([1.0, 1.0], dtype=np.float32),
np.asarray([0.0, 1.0], dtype=np.float32),
)
aux_high = (
np.asarray([1.0, -1.0], dtype=np.float32),
np.asarray([1.0, 1.0], dtype=np.float32),
)
phase_low, phase_high = compute_do1_tagged_phase_curves(aux_low, aux_high)
self.assertIsNotNone(phase_low)
self.assertIsNotNone(phase_high)
self.assertTrue(np.allclose(phase_low, np.asarray([0.0, np.pi / 4.0], dtype=np.float32), atol=1e-6))
self.assertTrue(np.allclose(phase_high, np.asarray([np.pi / 4.0, 3.0 * np.pi / 4.0], dtype=np.float32), atol=1e-6))
def test_decimate_curve_for_display_preserves_small_series(self): def test_decimate_curve_for_display_preserves_small_series(self):
xs = np.linspace(3.3, 14.3, 64, dtype=np.float64) xs = np.linspace(3.3, 14.3, 64, dtype=np.float64)
ys = np.linspace(-1.0, 1.0, 64, dtype=np.float32) ys = np.linspace(-1.0, 1.0, 64, dtype=np.float32)

View File

@ -87,6 +87,25 @@ def _pack_tty_point(step: int, ch1: int, ch2: int) -> bytes:
) )
def _pack_tty_tagged_point(marker_word0: int, step: int, ch1: int, ch2: int) -> bytes:
return b"".join(
[
_u16le(marker_word0),
_u16le(step),
_u16le(ch1),
_u16le(ch2),
]
)
def _pack_tty_tagged_low_point(step: int, ch1: int, ch2: int) -> bytes:
return _pack_tty_tagged_point(0x00A3, step, ch1, ch2)
def _pack_tty_tagged_high_point(step: int, ch1: int, ch2: int) -> bytes:
return _pack_tty_tagged_point(0x00A4, step, ch1, ch2)
def _pack_logdet_point(step: int, value: int) -> bytes: def _pack_logdet_point(step: int, value: int) -> bytes:
return b"".join( return b"".join(
[ [
@ -189,6 +208,78 @@ class SweepParserCoreTests(unittest.TestCase):
self.assertEqual(events[4].aux, (120.0, 80.0)) self.assertEqual(events[4].aux, (120.0, 80.0))
self.assertEqual(events[4].signal_kind, "bin_iq") self.assertEqual(events[4].signal_kind, "bin_iq")
def test_legacy_binary_parser_accepts_tty_do1_tagged_stream(self):
parser = LegacyBinaryParser()
stream = b"".join(
[
_pack_tty_start(),
_pack_tty_tagged_low_point(1, 100, 90),
_pack_tty_tagged_high_point(1, 120, 95),
]
)
events = parser.feed(stream)
self.assertEqual(len(events), 3)
self.assertIsInstance(events[0], StartEvent)
self.assertEqual(events[0].signal_kind, "bin_iq")
self.assertIsInstance(events[1], PointEvent)
self.assertEqual(events[1].signal_kind, "bin_iq_do1_tagged")
self.assertEqual(events[1].do1_level, "low")
self.assertEqual(events[1].x, 1)
self.assertEqual(events[1].aux, (100.0, 90.0))
self.assertIsInstance(events[2], PointEvent)
self.assertEqual(events[2].signal_kind, "bin_iq_do1_tagged")
self.assertEqual(events[2].do1_level, "high")
self.assertEqual(events[2].x, 1)
self.assertEqual(events[2].aux, (120.0, 95.0))
def test_legacy_binary_parser_keeps_same_step_for_different_do1_levels_in_one_sweep(self):
parser = LegacyBinaryParser()
stream = b"".join(
[
_pack_tty_start(),
_pack_tty_tagged_low_point(1, 100, 90),
_pack_tty_tagged_high_point(1, 120, 95),
_pack_tty_tagged_low_point(2, 130, 80),
_pack_tty_tagged_high_point(2, 140, 75),
]
)
events = parser.feed(stream)
start_events = [event for event in events if isinstance(event, StartEvent)]
self.assertEqual(len(start_events), 1)
self.assertEqual(start_events[0].signal_kind, "bin_iq")
point_levels = [event.do1_level for event in events if isinstance(event, PointEvent)]
self.assertEqual(point_levels, ["low", "high", "low", "high"])
def test_legacy_binary_parser_resets_tagged_stream_only_on_same_level_step_reset(self):
parser = LegacyBinaryParser()
stream = b"".join(
[
_pack_tty_start(),
_pack_tty_tagged_low_point(1, 100, 90),
_pack_tty_tagged_high_point(1, 120, 95),
_pack_tty_tagged_low_point(2, 130, 80),
_pack_tty_tagged_high_point(2, 140, 75),
_pack_tty_tagged_low_point(1, 110, 85),
]
)
events = parser.feed(stream)
self.assertIsInstance(events[0], StartEvent)
self.assertIsInstance(events[1], PointEvent)
self.assertIsInstance(events[2], PointEvent)
self.assertIsInstance(events[3], PointEvent)
self.assertIsInstance(events[4], PointEvent)
self.assertIsInstance(events[5], StartEvent)
self.assertEqual(events[5].signal_kind, "bin_iq_do1_tagged")
self.assertIsInstance(events[6], PointEvent)
self.assertEqual(events[6].do1_level, "low")
self.assertEqual(events[6].x, 1)
def test_legacy_binary_parser_tty_mode_does_not_flip_to_legacy_on_ch2_low_byte_0x0a(self): def test_legacy_binary_parser_tty_mode_does_not_flip_to_legacy_on_ch2_low_byte_0x0a(self):
parser = LegacyBinaryParser() parser = LegacyBinaryParser()
stream = b"".join( stream = b"".join(
@ -377,6 +468,40 @@ class SweepParserCoreTests(unittest.TestCase):
self.assertEqual(aux[0][1], 100.0) self.assertEqual(aux[0][1], 100.0)
self.assertEqual(aux[1][2], 95.0) self.assertEqual(aux[1][2], 95.0)
def test_sweep_assembler_builds_tagged_payload_and_nanmean_aggregate(self):
assembler = SweepAssembler(fancy=False, apply_inversion=False)
self.assertIsNone(assembler.consume(StartEvent(ch=0, signal_kind="bin_iq_do1_tagged")))
assembler.consume(
PointEvent(
ch=0,
x=1,
y=10.0,
aux=(100.0, 90.0),
signal_kind="bin_iq_do1_tagged",
do1_level="low",
)
)
assembler.consume(
PointEvent(
ch=0,
x=1,
y=20.0,
aux=(120.0, 95.0),
signal_kind="bin_iq_do1_tagged",
do1_level="high",
)
)
sweep, info, aux = assembler.finalize_current()
self.assertIsNone(aux)
self.assertEqual(info["signal_kind"], "bin_iq_do1_tagged")
self.assertAlmostEqual(float(sweep[1]), 15.0, places=6)
payload = info.get("_do1_tagged_payload")
self.assertIsInstance(payload, dict)
self.assertIn("raw_low", payload)
self.assertIn("raw_high", payload)
self.assertIn("aux_low", payload)
self.assertIn("aux_high", payload)
def test_sweep_assembler_splits_packet_on_channel_switch(self): def test_sweep_assembler_splits_packet_on_channel_switch(self):
assembler = SweepAssembler(fancy=False, apply_inversion=False) assembler = SweepAssembler(fancy=False, apply_inversion=False)
self.assertIsNone(assembler.consume(PointEvent(ch=1, x=1, y=10.0))) self.assertIsNone(assembler.consume(PointEvent(ch=1, x=1, y=10.0)))

View File

@ -66,6 +66,25 @@ def _pack_tty_point(step: int, ch1: int, ch2: int) -> bytes:
) )
def _pack_tty_tagged_point(marker_word0: int, step: int, ch1: int, ch2: int) -> bytes:
return b"".join(
[
_u16le(marker_word0),
_u16le(step),
_u16le(ch1),
_u16le(ch2),
]
)
def _pack_tty_tagged_low(step: int, ch1: int, ch2: int) -> bytes:
return _pack_tty_tagged_point(0x00A3, step, ch1, ch2)
def _pack_tty_tagged_high(step: int, ch1: int, ch2: int) -> bytes:
return _pack_tty_tagged_point(0x00A4, step, ch1, ch2)
def _pack_logdet_point(step: int, value: int) -> bytes: def _pack_logdet_point(step: int, value: int) -> bytes:
return b"".join( return b"".join(
[ [
@ -166,6 +185,29 @@ class SweepReaderTests(unittest.TestCase):
reader.join(timeout=1.0) reader.join(timeout=1.0)
stack.close() stack.close()
def test_parser_16_bit_x2_falls_back_to_tty_do1_tagged_stream(self):
payload = bytearray()
while len(payload) < (_PARSER_16_BIT_X2_PROBE_BYTES + 40):
payload += _pack_tty_start()
payload += _pack_tty_tagged_low(1, 100, 90)
payload += _pack_tty_tagged_high(1, 120, 95)
payload += _pack_tty_tagged_low(2, 110, 80)
payload += _pack_tty_tagged_high(2, 130, 70)
payload += _pack_tty_tagged_low(1, 105, 85)
stack, reader, queue, stop_event, stderr = self._start_reader(bytes(payload), parser_16_bit_x2=True)
try:
sweep, info, aux = queue.get(timeout=2.0)
self.assertEqual(info["signal_kind"], "bin_iq_do1_tagged")
self.assertIsNone(aux)
self.assertIn("_do1_tagged_payload", info)
self.assertGreaterEqual(sweep.shape[0], 2)
self.assertIn("fallback -> legacy", stderr.getvalue())
finally:
stop_event.set()
reader.join(timeout=1.0)
stack.close()
def test_parser_16_bit_x2_keeps_true_complex_stream(self): def test_parser_16_bit_x2_keeps_true_complex_stream(self):
payload = b"".join( payload = b"".join(
[ [