new
This commit is contained in:
6
rfg_adc_plotter/io/__init__.py
Normal file
6
rfg_adc_plotter/io/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""I/O helpers for serial sources and sweep parsing."""
|
||||
|
||||
from rfg_adc_plotter.io.serial_source import SerialChunkReader, SerialLineSource
|
||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||
|
||||
__all__ = ["SerialChunkReader", "SerialLineSource", "SweepReader"]
|
||||
177
rfg_adc_plotter/io/serial_source.py
Normal file
177
rfg_adc_plotter/io/serial_source.py
Normal file
@ -0,0 +1,177 @@
|
||||
"""Serial input helpers with pyserial and raw TTY fallbacks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def try_open_pyserial(path: str, baud: int, timeout: float):
|
||||
try:
|
||||
import serial # type: ignore
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
return serial.Serial(path, baudrate=baud, timeout=timeout)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
class FDReader:
|
||||
"""Buffered wrapper around a raw TTY file descriptor."""
|
||||
|
||||
def __init__(self, fd: int):
|
||||
self._fd = fd
|
||||
raw = os.fdopen(fd, "rb", closefd=False)
|
||||
self._file = raw
|
||||
self._buf = io.BufferedReader(raw, buffer_size=65536)
|
||||
|
||||
def fileno(self) -> int:
|
||||
return self._fd
|
||||
|
||||
def readline(self) -> bytes:
|
||||
return self._buf.readline()
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self._buf.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def open_raw_tty(path: str, baud: int) -> Optional[FDReader]:
|
||||
"""Open a TTY without pyserial and configure it via termios."""
|
||||
try:
|
||||
import termios
|
||||
import tty
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NOCTTY)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
attrs = termios.tcgetattr(fd)
|
||||
tty.setraw(fd)
|
||||
|
||||
baud_map = {
|
||||
9600: termios.B9600,
|
||||
19200: termios.B19200,
|
||||
38400: termios.B38400,
|
||||
57600: termios.B57600,
|
||||
115200: termios.B115200,
|
||||
230400: getattr(termios, "B230400", None),
|
||||
460800: getattr(termios, "B460800", None),
|
||||
}
|
||||
speed = baud_map.get(baud) or termios.B115200
|
||||
|
||||
attrs[4] = speed
|
||||
attrs[5] = speed
|
||||
cc = attrs[6]
|
||||
cc[termios.VMIN] = 1
|
||||
cc[termios.VTIME] = 0
|
||||
attrs[6] = cc
|
||||
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
||||
except Exception:
|
||||
try:
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
return FDReader(fd)
|
||||
|
||||
|
||||
class SerialLineSource:
|
||||
"""Unified line-oriented wrapper for pyserial and raw TTY readers."""
|
||||
|
||||
def __init__(self, path: str, baud: int, timeout: float = 1.0):
|
||||
self._pyserial = try_open_pyserial(path, baud, timeout)
|
||||
self._fdreader: Optional[FDReader] = None
|
||||
self._using = "pyserial" if self._pyserial is not None else "raw"
|
||||
if self._pyserial is None:
|
||||
self._fdreader = open_raw_tty(path, baud)
|
||||
if self._fdreader is None:
|
||||
msg = f"Не удалось открыть порт '{path}' (pyserial и raw TTY не сработали)"
|
||||
if sys.platform.startswith("win"):
|
||||
msg += ". На Windows нужен pyserial: pip install pyserial"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def readline(self) -> bytes:
|
||||
if self._pyserial is not None:
|
||||
try:
|
||||
return self._pyserial.readline()
|
||||
except Exception:
|
||||
return b""
|
||||
try:
|
||||
return self._fdreader.readline() # type: ignore[union-attr]
|
||||
except Exception:
|
||||
return b""
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
if self._pyserial is not None:
|
||||
self._pyserial.close()
|
||||
elif self._fdreader is not None:
|
||||
self._fdreader.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class SerialChunkReader:
|
||||
"""Fast non-blocking chunk reader for serial sources."""
|
||||
|
||||
def __init__(self, src: SerialLineSource):
|
||||
self._src = src
|
||||
self._ser = src._pyserial
|
||||
self._fd: Optional[int] = None
|
||||
if self._ser is not None:
|
||||
try:
|
||||
self._ser.timeout = 0
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self._fd = src._fdreader.fileno() # type: ignore[union-attr]
|
||||
try:
|
||||
os.set_blocking(self._fd, False)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
self._fd = None
|
||||
|
||||
def read_available(self) -> bytes:
|
||||
"""Return currently available bytes or b"" when nothing is ready."""
|
||||
if self._ser is not None:
|
||||
try:
|
||||
available = int(getattr(self._ser, "in_waiting", 0))
|
||||
except Exception:
|
||||
available = 0
|
||||
if available > 0:
|
||||
try:
|
||||
return self._ser.read(available)
|
||||
except Exception:
|
||||
return b""
|
||||
return b""
|
||||
|
||||
if self._fd is None:
|
||||
return b""
|
||||
|
||||
out = bytearray()
|
||||
while True:
|
||||
try:
|
||||
chunk = os.read(self._fd, 65536)
|
||||
if not chunk:
|
||||
break
|
||||
out += chunk
|
||||
if len(chunk) < 65536:
|
||||
break
|
||||
except BlockingIOError:
|
||||
break
|
||||
except Exception:
|
||||
break
|
||||
return bytes(out)
|
||||
425
rfg_adc_plotter/io/sweep_parser_core.py
Normal file
425
rfg_adc_plotter/io/sweep_parser_core.py
Normal file
@ -0,0 +1,425 @@
|
||||
"""Reusable sweep parsers and sweep assembly helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import 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 ParserEvent, PointEvent, StartEvent, SweepAuxCurves, SweepInfo, SweepPacket
|
||||
|
||||
|
||||
def u32_to_i32(value: int) -> int:
|
||||
return value - 0x1_0000_0000 if (value & 0x8000_0000) else value
|
||||
|
||||
|
||||
def u16_to_i16(value: int) -> int:
|
||||
return value - 0x1_0000 if (value & 0x8000) else value
|
||||
|
||||
|
||||
def log_value_to_linear(value: int) -> float:
|
||||
exponent = max(-LOG_EXP_LIMIT, min(LOG_EXP_LIMIT, float(value) * LOG_SCALER))
|
||||
return float(LOG_BASE ** exponent)
|
||||
|
||||
|
||||
def log_pair_to_sweep(avg_1: int, avg_2: int) -> float:
|
||||
return (log_value_to_linear(avg_1) - log_value_to_linear(avg_2)) * LOG_POSTSCALER
|
||||
|
||||
|
||||
class AsciiSweepParser:
|
||||
"""Incremental parser for ASCII sweep streams."""
|
||||
|
||||
def __init__(self):
|
||||
self._buf = bytearray()
|
||||
|
||||
def feed(self, data: bytes) -> List[ParserEvent]:
|
||||
if data:
|
||||
self._buf += data
|
||||
events: List[ParserEvent] = []
|
||||
while True:
|
||||
nl = self._buf.find(b"\n")
|
||||
if nl == -1:
|
||||
break
|
||||
line = bytes(self._buf[:nl])
|
||||
del self._buf[: nl + 1]
|
||||
if line.endswith(b"\r"):
|
||||
line = line[:-1]
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith(b"Sweep_start"):
|
||||
events.append(StartEvent())
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
head = parts[0].lower()
|
||||
try:
|
||||
if head == b"s":
|
||||
if len(parts) >= 4:
|
||||
ch = int(parts[1], 10)
|
||||
x = int(parts[2], 10)
|
||||
y = int(parts[3], 10)
|
||||
else:
|
||||
ch = 0
|
||||
x = int(parts[1], 10)
|
||||
y = int(parts[2], 10)
|
||||
elif head.startswith(b"s"):
|
||||
ch = int(head[1:], 10)
|
||||
x = int(parts[1], 10)
|
||||
y = int(parts[2], 10)
|
||||
else:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
events.append(PointEvent(ch=int(ch), x=int(x), y=float(y)))
|
||||
return events
|
||||
|
||||
|
||||
class LegacyBinaryParser:
|
||||
"""Byte-resynchronizing parser for legacy 8-byte binary records."""
|
||||
|
||||
def __init__(self):
|
||||
self._buf = bytearray()
|
||||
|
||||
@staticmethod
|
||||
def _u16_at(buf: bytearray, offset: int) -> int:
|
||||
return int(buf[offset]) | (int(buf[offset + 1]) << 8)
|
||||
|
||||
def feed(self, data: bytes) -> List[ParserEvent]:
|
||||
if data:
|
||||
self._buf += data
|
||||
events: List[ParserEvent] = []
|
||||
while len(self._buf) >= 8:
|
||||
w0 = self._u16_at(self._buf, 0)
|
||||
w1 = self._u16_at(self._buf, 2)
|
||||
w2 = self._u16_at(self._buf, 4)
|
||||
if w0 == 0xFFFF and w1 == 0xFFFF and w2 == 0xFFFF and self._buf[6] == 0x0A:
|
||||
events.append(StartEvent(ch=int(self._buf[7])))
|
||||
del self._buf[:8]
|
||||
continue
|
||||
if self._buf[6] == 0x0A:
|
||||
ch = int(self._buf[7])
|
||||
value = u32_to_i32((w1 << 16) | w2)
|
||||
events.append(PointEvent(ch=ch, x=int(w0), y=float(value)))
|
||||
del self._buf[:8]
|
||||
continue
|
||||
del self._buf[:1]
|
||||
return events
|
||||
|
||||
|
||||
class LogScaleBinaryParser32:
|
||||
"""Byte-resynchronizing parser for 32-bit logscale pair records."""
|
||||
|
||||
def __init__(self):
|
||||
self._buf = bytearray()
|
||||
|
||||
@staticmethod
|
||||
def _u16_at(buf: bytearray, offset: int) -> int:
|
||||
return int(buf[offset]) | (int(buf[offset + 1]) << 8)
|
||||
|
||||
def feed(self, data: bytes) -> List[ParserEvent]:
|
||||
if data:
|
||||
self._buf += data
|
||||
events: List[ParserEvent] = []
|
||||
while len(self._buf) >= 12:
|
||||
words = [self._u16_at(self._buf, idx * 2) for idx in range(6)]
|
||||
if words[0:5] == [0xFFFF] * 5 and (words[5] & 0x00FF) == 0x000A:
|
||||
events.append(StartEvent(ch=int((words[5] >> 8) & 0x00FF)))
|
||||
del self._buf[:12]
|
||||
continue
|
||||
if (words[5] & 0x00FF) == 0x000A and words[0] != 0xFFFF:
|
||||
ch = int((words[5] >> 8) & 0x00FF)
|
||||
avg_1 = u32_to_i32((words[1] << 16) | words[2])
|
||||
avg_2 = u32_to_i32((words[3] << 16) | words[4])
|
||||
events.append(
|
||||
PointEvent(
|
||||
ch=ch,
|
||||
x=int(words[0]),
|
||||
y=log_pair_to_sweep(avg_1, avg_2),
|
||||
aux=(float(avg_1), float(avg_2)),
|
||||
)
|
||||
)
|
||||
del self._buf[:12]
|
||||
continue
|
||||
del self._buf[:1]
|
||||
return events
|
||||
|
||||
|
||||
class LogScale16BitX2BinaryParser:
|
||||
"""Byte-resynchronizing parser for 16-bit x2 logscale records."""
|
||||
|
||||
def __init__(self):
|
||||
self._buf = bytearray()
|
||||
self._current_channel = 0
|
||||
|
||||
@staticmethod
|
||||
def _u16_at(buf: bytearray, offset: int) -> int:
|
||||
return int(buf[offset]) | (int(buf[offset + 1]) << 8)
|
||||
|
||||
def feed(self, data: bytes) -> List[ParserEvent]:
|
||||
if data:
|
||||
self._buf += data
|
||||
events: List[ParserEvent] = []
|
||||
while len(self._buf) >= 8:
|
||||
words = [self._u16_at(self._buf, idx * 2) for idx in range(4)]
|
||||
if words[0:3] == [0xFFFF, 0xFFFF, 0xFFFF] and (words[3] & 0x00FF) == 0x000A:
|
||||
self._current_channel = int((words[3] >> 8) & 0x00FF)
|
||||
events.append(StartEvent(ch=self._current_channel))
|
||||
del self._buf[:8]
|
||||
continue
|
||||
if words[3] == 0xFFFF and words[0] != 0xFFFF:
|
||||
avg_1 = u16_to_i16(words[1])
|
||||
avg_2 = u16_to_i16(words[2])
|
||||
events.append(
|
||||
PointEvent(
|
||||
ch=self._current_channel,
|
||||
x=int(words[0]),
|
||||
y=log_pair_to_sweep(avg_1, avg_2),
|
||||
aux=(float(avg_1), float(avg_2)),
|
||||
)
|
||||
)
|
||||
del self._buf[:8]
|
||||
continue
|
||||
del self._buf[:1]
|
||||
return events
|
||||
|
||||
|
||||
class ParserTestStreamParser:
|
||||
"""Parser for the special test 16-bit x2 stream format."""
|
||||
|
||||
def __init__(self):
|
||||
self._buf = bytearray()
|
||||
self._buf_pos = 0
|
||||
self._point_buf: list[int] = []
|
||||
self._ffff_run = 0
|
||||
self._current_channel = 0
|
||||
self._expected_step: Optional[int] = None
|
||||
self._in_sweep = False
|
||||
self._local_resync = False
|
||||
|
||||
def _consume_point(self) -> Optional[PointEvent]:
|
||||
if len(self._point_buf) != 3:
|
||||
return None
|
||||
step = int(self._point_buf[0])
|
||||
if step <= 0:
|
||||
return None
|
||||
if self._expected_step is not None and step < self._expected_step:
|
||||
return None
|
||||
avg_1 = u16_to_i16(int(self._point_buf[1]))
|
||||
avg_2 = u16_to_i16(int(self._point_buf[2]))
|
||||
self._expected_step = step + 1
|
||||
return PointEvent(
|
||||
ch=self._current_channel,
|
||||
x=step,
|
||||
y=log_pair_to_sweep(avg_1, avg_2),
|
||||
aux=(float(avg_1), float(avg_2)),
|
||||
)
|
||||
|
||||
def feed(self, data: bytes) -> List[ParserEvent]:
|
||||
if data:
|
||||
self._buf += data
|
||||
events: List[ParserEvent] = []
|
||||
|
||||
while (self._buf_pos + 1) < len(self._buf):
|
||||
word = int(self._buf[self._buf_pos]) | (int(self._buf[self._buf_pos + 1]) << 8)
|
||||
self._buf_pos += 2
|
||||
|
||||
if word == 0xFFFF:
|
||||
self._ffff_run += 1
|
||||
continue
|
||||
|
||||
if self._ffff_run > 0:
|
||||
bad_point_on_delim = False
|
||||
if self._in_sweep and self._point_buf and not self._local_resync:
|
||||
point = self._consume_point()
|
||||
if point is None:
|
||||
self._local_resync = True
|
||||
bad_point_on_delim = True
|
||||
else:
|
||||
events.append(point)
|
||||
self._point_buf.clear()
|
||||
|
||||
if self._ffff_run >= 2:
|
||||
if (word & 0x00FF) == 0x000A:
|
||||
self._current_channel = (word >> 8) & 0x00FF
|
||||
self._in_sweep = True
|
||||
self._expected_step = 1
|
||||
self._local_resync = False
|
||||
self._point_buf.clear()
|
||||
events.append(StartEvent(ch=self._current_channel))
|
||||
self._ffff_run = 0
|
||||
continue
|
||||
if self._in_sweep:
|
||||
self._local_resync = True
|
||||
self._ffff_run = 0
|
||||
continue
|
||||
|
||||
if self._local_resync and not bad_point_on_delim:
|
||||
self._local_resync = False
|
||||
self._point_buf.clear()
|
||||
self._ffff_run = 0
|
||||
|
||||
if self._in_sweep and not self._local_resync:
|
||||
self._point_buf.append(word)
|
||||
if len(self._point_buf) > 3:
|
||||
self._point_buf.clear()
|
||||
self._local_resync = True
|
||||
|
||||
if self._buf_pos >= 262144:
|
||||
del self._buf[: self._buf_pos]
|
||||
self._buf_pos = 0
|
||||
if (len(self._buf) - self._buf_pos) > 1_000_000:
|
||||
tail = self._buf[self._buf_pos :]
|
||||
if len(tail) > 262144:
|
||||
tail = tail[-262144:]
|
||||
self._buf = bytearray(tail)
|
||||
self._buf_pos = 0
|
||||
return events
|
||||
|
||||
|
||||
class SweepAssembler:
|
||||
"""Collect parser events into sweep packets matching runtime expectations."""
|
||||
|
||||
def __init__(self, fancy: bool = False, apply_inversion: bool = True):
|
||||
self._fancy = bool(fancy)
|
||||
self._apply_inversion = bool(apply_inversion)
|
||||
self._max_width = 0
|
||||
self._sweep_idx = 0
|
||||
self._last_sweep_ts: Optional[float] = None
|
||||
self._n_valid_hist = deque()
|
||||
self._xs: list[int] = []
|
||||
self._ys: list[float] = []
|
||||
self._aux_1: list[float] = []
|
||||
self._aux_2: list[float] = []
|
||||
self._cur_channel: Optional[int] = None
|
||||
self._cur_channels: set[int] = set()
|
||||
|
||||
def _reset_current(self) -> None:
|
||||
self._xs.clear()
|
||||
self._ys.clear()
|
||||
self._aux_1.clear()
|
||||
self._aux_2.clear()
|
||||
self._cur_channel = None
|
||||
self._cur_channels.clear()
|
||||
|
||||
def _scatter(self, xs: Sequence[int], values: Sequence[float], width: int) -> np.ndarray:
|
||||
series = np.full((width,), np.nan, dtype=np.float32)
|
||||
try:
|
||||
idx = np.asarray(xs, dtype=np.int64)
|
||||
vals = np.asarray(values, dtype=np.float32)
|
||||
series[idx] = vals
|
||||
except Exception:
|
||||
for x, y in zip(xs, values):
|
||||
xi = int(x)
|
||||
if 0 <= xi < width:
|
||||
series[xi] = float(y)
|
||||
return series
|
||||
|
||||
@staticmethod
|
||||
def _fill_missing(series: np.ndarray) -> None:
|
||||
known = ~np.isnan(series)
|
||||
if not np.any(known):
|
||||
return
|
||||
known_idx = np.nonzero(known)[0]
|
||||
for i0, i1 in zip(known_idx[:-1], known_idx[1:]):
|
||||
if i1 - i0 > 1:
|
||||
avg = (series[i0] + series[i1]) * 0.5
|
||||
series[i0 + 1 : i1] = avg
|
||||
first_idx = int(known_idx[0])
|
||||
last_idx = int(known_idx[-1])
|
||||
if first_idx > 0:
|
||||
series[:first_idx] = series[first_idx]
|
||||
if last_idx < series.size - 1:
|
||||
series[last_idx + 1 :] = series[last_idx]
|
||||
|
||||
def consume(self, event: ParserEvent) -> Optional[SweepPacket]:
|
||||
if isinstance(event, StartEvent):
|
||||
packet = self.finalize_current()
|
||||
self._reset_current()
|
||||
if event.ch is not None:
|
||||
self._cur_channel = int(event.ch)
|
||||
self._cur_channels.add(int(event.ch))
|
||||
return packet
|
||||
|
||||
if self._cur_channel is None:
|
||||
self._cur_channel = int(event.ch)
|
||||
self._cur_channels.add(int(event.ch))
|
||||
self._xs.append(int(event.x))
|
||||
self._ys.append(float(event.y))
|
||||
if event.aux is not None:
|
||||
self._aux_1.append(float(event.aux[0]))
|
||||
self._aux_2.append(float(event.aux[1]))
|
||||
return None
|
||||
|
||||
def finalize_current(self) -> Optional[SweepPacket]:
|
||||
if not self._xs:
|
||||
return None
|
||||
|
||||
ch_list = sorted(self._cur_channels) if self._cur_channels else [0]
|
||||
ch_primary = ch_list[0] if ch_list else 0
|
||||
width = max(int(max(self._xs)) + 1, 1)
|
||||
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),
|
||||
)
|
||||
|
||||
n_valid_cur = int(np.count_nonzero(np.isfinite(sweep)))
|
||||
|
||||
if self._fancy:
|
||||
self._fill_missing(sweep)
|
||||
if aux_curves is not None:
|
||||
self._fill_missing(aux_curves[0])
|
||||
self._fill_missing(aux_curves[1])
|
||||
|
||||
if self._apply_inversion:
|
||||
try:
|
||||
mean_value = float(np.nanmean(sweep))
|
||||
if np.isfinite(mean_value) and mean_value < DATA_INVERSION_THRESHOLD:
|
||||
sweep *= -1.0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._sweep_idx += 1
|
||||
now = time.time()
|
||||
if self._last_sweep_ts is None:
|
||||
dt_ms = float("nan")
|
||||
else:
|
||||
dt_ms = (now - self._last_sweep_ts) * 1000.0
|
||||
self._last_sweep_ts = now
|
||||
|
||||
self._n_valid_hist.append((now, n_valid_cur))
|
||||
while self._n_valid_hist and (now - self._n_valid_hist[0][0]) > 1.0:
|
||||
self._n_valid_hist.popleft()
|
||||
n_valid = float(sum(value for _ts, value in self._n_valid_hist) / len(self._n_valid_hist))
|
||||
|
||||
if n_valid_cur > 0:
|
||||
vmin = float(np.nanmin(sweep))
|
||||
vmax = float(np.nanmax(sweep))
|
||||
mean = float(np.nanmean(sweep))
|
||||
std = float(np.nanstd(sweep))
|
||||
else:
|
||||
vmin = vmax = mean = std = float("nan")
|
||||
|
||||
info: SweepInfo = {
|
||||
"sweep": self._sweep_idx,
|
||||
"ch": ch_primary,
|
||||
"chs": ch_list,
|
||||
"n_valid": n_valid,
|
||||
"min": vmin,
|
||||
"max": vmax,
|
||||
"mean": mean,
|
||||
"std": std,
|
||||
"dt_ms": dt_ms,
|
||||
}
|
||||
return (sweep, info, aux_curves)
|
||||
102
rfg_adc_plotter/io/sweep_reader.py
Normal file
102
rfg_adc_plotter/io/sweep_reader.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Background sweep reader thread."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from queue import Full, Queue
|
||||
|
||||
from rfg_adc_plotter.io.serial_source import SerialChunkReader, SerialLineSource
|
||||
from rfg_adc_plotter.io.sweep_parser_core import (
|
||||
AsciiSweepParser,
|
||||
LegacyBinaryParser,
|
||||
LogScale16BitX2BinaryParser,
|
||||
LogScaleBinaryParser32,
|
||||
ParserTestStreamParser,
|
||||
SweepAssembler,
|
||||
)
|
||||
from rfg_adc_plotter.types import SweepPacket
|
||||
|
||||
|
||||
class SweepReader(threading.Thread):
|
||||
"""Read a serial source in the background and emit completed sweep packets."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port_path: str,
|
||||
baud: int,
|
||||
out_queue: "Queue[SweepPacket]",
|
||||
stop_event: threading.Event,
|
||||
fancy: bool = False,
|
||||
bin_mode: bool = False,
|
||||
logscale: bool = False,
|
||||
parser_16_bit_x2: bool = False,
|
||||
parser_test: bool = False,
|
||||
):
|
||||
super().__init__(daemon=True)
|
||||
self._port_path = port_path
|
||||
self._baud = int(baud)
|
||||
self._queue = out_queue
|
||||
self._stop = stop_event
|
||||
self._fancy = bool(fancy)
|
||||
self._bin_mode = bool(bin_mode)
|
||||
self._logscale = bool(logscale)
|
||||
self._parser_16_bit_x2 = bool(parser_16_bit_x2)
|
||||
self._parser_test = bool(parser_test)
|
||||
self._src: SerialLineSource | None = None
|
||||
|
||||
def _build_parser(self):
|
||||
if self._parser_test:
|
||||
return ParserTestStreamParser(), SweepAssembler(fancy=self._fancy, apply_inversion=False)
|
||||
if self._parser_16_bit_x2:
|
||||
return LogScale16BitX2BinaryParser(), SweepAssembler(fancy=self._fancy, apply_inversion=False)
|
||||
if self._logscale:
|
||||
return LogScaleBinaryParser32(), SweepAssembler(fancy=self._fancy, apply_inversion=False)
|
||||
if self._bin_mode:
|
||||
return LegacyBinaryParser(), SweepAssembler(fancy=self._fancy, apply_inversion=True)
|
||||
return AsciiSweepParser(), SweepAssembler(fancy=self._fancy, apply_inversion=True)
|
||||
|
||||
def _enqueue(self, packet: SweepPacket) -> None:
|
||||
try:
|
||||
self._queue.put_nowait(packet)
|
||||
except Full:
|
||||
try:
|
||||
_ = self._queue.get_nowait()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._queue.put_nowait(packet)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
self._src = SerialLineSource(self._port_path, self._baud, timeout=1.0)
|
||||
sys.stderr.write(f"[info] Открыл порт {self._port_path} ({self._src._using})\n")
|
||||
except Exception as exc:
|
||||
sys.stderr.write(f"[error] {exc}\n")
|
||||
return
|
||||
|
||||
parser, assembler = self._build_parser()
|
||||
|
||||
try:
|
||||
chunk_reader = SerialChunkReader(self._src)
|
||||
while not self._stop.is_set():
|
||||
data = chunk_reader.read_available()
|
||||
if not data:
|
||||
time.sleep(0.0005)
|
||||
continue
|
||||
for event in parser.feed(data):
|
||||
packet = assembler.consume(event)
|
||||
if packet is not None:
|
||||
self._enqueue(packet)
|
||||
packet = assembler.finalize_current()
|
||||
if packet is not None:
|
||||
self._enqueue(packet)
|
||||
finally:
|
||||
try:
|
||||
if self._src is not None:
|
||||
self._src.close()
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user