something working new format

This commit is contained in:
awe
2026-03-05 17:56:27 +03:00
parent 1e05b1f3fd
commit 26c3dd7ad5
10 changed files with 320 additions and 223 deletions

View File

@ -46,31 +46,23 @@ def detect_reference_file_format(path: str) -> Optional[str]:
size = os.path.getsize(p)
except Exception:
return None
if size <= 0 or (size % 8) != 0:
if size <= 0:
return None
try:
with open(p, "rb") as f:
sample = f.read(min(size, 8 * 2048))
sample = f.read(min(size, 256 * 1024))
except Exception:
return None
if len(sample) < 8:
return None
# Быстрый sniff aligned-записей: в валидных записях байт 6 == 0x0A.
recs = len(sample) // 8
if recs <= 0:
return None
marker_hits = 0
start_hits = 0
for i in range(0, recs * 8, 8):
b = sample[i : i + 8]
if b[6] == 0x0A:
marker_hits += 1
if b[:6] == b"\xff\xff\xff\xff\xff\xff":
start_hits += 1
if marker_hits >= max(4, int(recs * 0.8)) and start_hits >= 1:
# Универсальный sniff: прогоняем тем же потоковым парсером,
# который используется в realtime/capture-import.
parser = BinaryRecordStreamParser()
_ = parser.feed(sample)
if parser.start_count >= 1 and parser.point_count >= 16:
return "bin_capture"
return None

View File

@ -2,9 +2,10 @@
from __future__ import annotations
import math
from collections import deque
import time
from typing import Iterable, List, Optional, Sequence, Set, Tuple
from typing import List, Optional, Sequence, Set, Tuple
import numpy as np
@ -14,7 +15,13 @@ from rfg_adc_plotter.types import SweepInfo, SweepPacket
# Binary parser events:
# ("start", ch)
# ("point", ch, x, y)
BinaryEvent = Tuple[str, int] | Tuple[str, int, int, int]
BinaryEvent = Tuple[str, int] | Tuple[str, int, int, float]
# Параметры преобразования пары log-detector значений в линейную амплитуду.
_LOG_DETECTOR_BASE = 10.0
_LOG_DETECTOR_SCALER = 0.001
_LOG_DETECTOR_POSTSCALE = 1000.0
_LOG_DETECTOR_EXP_LIMIT = 300.0
def u32_to_i32(v: int) -> int:
@ -22,8 +29,44 @@ def u32_to_i32(v: int) -> int:
return v - 0x1_0000_0000 if (v & 0x8000_0000) else v
def u_bits_to_i(v: int, bits: int) -> int:
"""Преобразование беззнакового целого fixed-width в знаковое (two's complement)."""
if bits <= 0:
return 0
sign = 1 << (bits - 1)
full = 1 << bits
return v - full if (v & sign) else v
def words_be_to_i(words: Sequence[int]) -> int:
"""Собрать big-endian набор 16-bit слов в знаковое число."""
acc = 0
for w in words:
acc = (acc << 16) | (int(w) & 0xFFFF)
return u_bits_to_i(acc, 16 * int(len(words)))
def _log_pair_to_linear(avg_1: int, avg_2: int) -> float:
"""Разность двух логарифмических усреднений в линейной шкале."""
exp1 = max(-_LOG_DETECTOR_EXP_LIMIT, min(_LOG_DETECTOR_EXP_LIMIT, float(avg_1) * _LOG_DETECTOR_SCALER))
exp2 = max(-_LOG_DETECTOR_EXP_LIMIT, min(_LOG_DETECTOR_EXP_LIMIT, float(avg_2) * _LOG_DETECTOR_SCALER))
return (math.pow(_LOG_DETECTOR_BASE, exp1) - math.pow(_LOG_DETECTOR_BASE, exp2)) * _LOG_DETECTOR_POSTSCALE
class BinaryRecordStreamParser:
"""Инкрементальный парсер бинарных записей протокола (по 8 байт)."""
"""Инкрементальный парсер бинарных записей нескольких wire-форматов.
Поддерживаемые форматы:
1) legacy 8-byte:
старт: 0xFFFF,0xFFFF,0xFFFF,(ch<<8)|0x0A
точка: step,value_hi16,value_lo16,(ch<<8)|0x0A
2) log-detector:
старт: 0xFFFF x5, (ch<<8)|0x0A
точка: step, avg1, avg2, (ch<<8)|0x0A,
где avg1/avg2 кодируются фиксированной шириной в 16-bit словах:
- 2 слова (int32) или
- 8 слов (int128).
"""
def __init__(self):
self._buf = bytearray()
@ -31,6 +74,49 @@ class BinaryRecordStreamParser:
self.start_count: int = 0
self.point_count: int = 0
self.desync_count: int = 0
self._log_pair_words: Optional[int] = None
@staticmethod
def _u16_at(buf: bytearray, offset: int) -> int:
return int(buf[offset]) | (int(buf[offset + 1]) << 8)
def _try_parse_log_start(self, buf: bytearray) -> Optional[Tuple[int, int]]:
rec_bytes = 12 # 6 слов: FFFF x5 + terminator
if len(buf) < rec_bytes:
return None
for wi in range(5):
if self._u16_at(buf, wi * 2) != 0xFFFF:
return None
term = self._u16_at(buf, 10)
if (term & 0x00FF) != 0x000A:
return None
ch = int((term >> 8) & 0x00FF)
return ch, rec_bytes
def _try_parse_log_point(self, buf: bytearray, pair_words: int) -> Optional[Tuple[int, int, float, int]]:
if pair_words <= 0:
return None
rec_words = 2 + 2 * int(pair_words)
rec_bytes = 2 * rec_words
if len(buf) < rec_bytes:
return None
step = self._u16_at(buf, 0)
if step == 0xFFFF:
return None
term_off = rec_bytes - 2
term = self._u16_at(buf, term_off)
if (term & 0x00FF) != 0x000A:
return None
a1_words = [self._u16_at(buf, 2 + 2 * i) for i in range(pair_words)]
a2_words = [self._u16_at(buf, 2 + 2 * (pair_words + i)) for i in range(pair_words)]
avg_1 = words_be_to_i(a1_words)
avg_2 = words_be_to_i(a2_words)
y_val = _log_pair_to_linear(avg_1, avg_2)
ch = int((term >> 8) & 0x00FF)
return ch, int(step), float(y_val), rec_bytes
def feed(self, data: bytes) -> List[BinaryEvent]:
if data:
@ -39,22 +125,57 @@ class BinaryRecordStreamParser:
buf = self._buf
while len(buf) >= 8:
w0 = int(buf[0]) | (int(buf[1]) << 8)
w1 = int(buf[2]) | (int(buf[3]) << 8)
w2 = int(buf[4]) | (int(buf[5]) << 8)
# 1) log-detector start (12-byte): FFFF x5 + (ch<<8)|0x0A
parsed_log_start = self._try_parse_log_start(buf)
if parsed_log_start is not None:
ch, consumed = parsed_log_start
events.append(("start", ch))
del buf[:consumed]
self.bytes_consumed += consumed
self.start_count += 1
# Ширину пары (32/128) определим на ближайшей точке.
self._log_pair_words = None
continue
# 2) log-detector point:
# сперва в уже известной ширине пары, иначе авто-детект 128/32.
# В авто-режиме сначала пробуем 32-bit пару (наиболее частый формат),
# затем 128-bit. Это снижает риск ложного совпадения 128-bit длины на 32-bit потоке.
pair_candidates = [self._log_pair_words] if self._log_pair_words in (2, 8) else [2, 8]
parsed_log_point: Optional[Tuple[int, int, float, int]] = None
for pair_words in pair_candidates:
if pair_words is None:
continue
parsed_log_point = self._try_parse_log_point(buf, int(pair_words))
if parsed_log_point is not None:
self._log_pair_words = int(pair_words)
break
if parsed_log_point is not None:
ch, step, y_val, consumed = parsed_log_point
events.append(("point", ch, step, y_val))
del buf[:consumed]
self.bytes_consumed += consumed
self.point_count += 1
continue
# 3) legacy 8-byte start / point.
w0 = self._u16_at(buf, 0)
w1 = self._u16_at(buf, 2)
w2 = self._u16_at(buf, 4)
if w0 == 0xFFFF and w1 == 0xFFFF and w2 == 0xFFFF and buf[6] == 0x0A:
ch = int(buf[7])
events.append(("start", ch))
del buf[:8]
self.bytes_consumed += 8
self.start_count += 1
# legacy не использует пару avg1/avg2.
self._log_pair_words = None
continue
if buf[6] == 0x0A:
ch = int(buf[7])
value_u32 = (w1 << 16) | w2
events.append(("point", ch, int(w0), u32_to_i32(value_u32)))
events.append(("point", ch, int(w0), float(u32_to_i32(value_u32))))
del buf[:8]
self.bytes_consumed += 8
self.point_count += 1
@ -88,7 +209,7 @@ class SweepAssembler:
self._n_valid_hist = deque()
self._xs: list[int] = []
self._ys: list[int] = []
self._ys: list[float] = []
self._cur_channel: Optional[int] = None
self._cur_channels: set[int] = set()
@ -98,12 +219,12 @@ class SweepAssembler:
self._cur_channel = None
self._cur_channels.clear()
def add_point(self, ch: int, x: int, y: int):
def add_point(self, ch: int, x: int, y: float):
if self._cur_channel is None:
self._cur_channel = int(ch)
self._cur_channels.add(int(ch))
self._xs.append(int(x))
self._ys.append(int(y))
self._ys.append(float(y))
def start_new_sweep(self, ch: int, now_ts: Optional[float] = None) -> Optional[SweepPacket]:
packet = self.finalize_current(now_ts=now_ts)
@ -122,13 +243,13 @@ class SweepAssembler:
return out
# point
_tag, ch, x, y = event # type: ignore[misc]
self.add_point(int(ch), int(x), int(y))
self.add_point(int(ch), int(x), float(y))
return out
def finalize_arrays(
self,
xs: Sequence[int],
ys: Sequence[int],
ys: Sequence[float],
channels: Optional[Set[int]],
now_ts: Optional[float] = None,
) -> Optional[SweepPacket]:

View File

@ -151,20 +151,17 @@ class SweepReader(threading.Thread):
def _run_binary_stream(self, chunk_reader: SerialChunkReader):
xs: list[int] = []
ys: list[int] = []
ys: list[float] = []
cur_channel: Optional[int] = None
cur_channels: set[int] = set()
parser = BinaryRecordStreamParser()
# Бинарный протокол (4 слова LE u16 = 8 байт на запись):
# старт свипа: 0xFFFF, 0xFFFF, 0xFFFF, (ch<<8)|0x0A
# Байты на проводе: ff ff ff ff ff ff 0a [ch]
# ch=0 → последнее слово=0x000A; ch=1 → 0x010A; и т.д.
# точка данных: step_u16, value_hi_u16, value_lo_u16, (ch<<8)|0x0A
# Байты на проводе: [step_lo step_hi] [hi_lo hi_hi] [lo_lo lo_hi] 0a [ch]
# value_i32 = sign_extend((value_hi<<16)|value_lo)
# Признак записи: байт 6 == 0x0A, байт 7 — номер канала.
# При десинхронизации сдвигаемся на 1 БАЙТ (не слово) для самосинхронизации.
# Поддерживаются оба wire-формата:
# 1) legacy: 8-byte записи (start/point с одним int32 значением).
# 2) log-detector: start = FFFF x5 + (ch<<8)|0x0A,
# point = step + (avg1, avg2), где avg1/avg2 имеют ширину 32-bit или 128-bit.
# Для point парсер сразу преобразует (avg1, avg2) в линейную амплитуду y.
# В обоих режимах при десинхронизации parser.feed() сдвигается на 1 байт.
_dbg_byte_count = 0
_dbg_desync_count = 0
@ -196,13 +193,13 @@ class SweepReader(threading.Thread):
_tag, ch_from_term, step, value_i32 = ev # type: ignore[misc]
if cur_channel is None:
cur_channel = int(ch_from_term)
cur_channels.add(int(cur_channel))
cur_channels.add(int(ch_from_term))
xs.append(int(step))
ys.append(int(value_i32))
ys.append(float(value_i32))
_dbg_point_count += 1
if self._debug and _dbg_point_count <= 3:
sys.stderr.write(
f"[debug] BIN точка: step={int(step)} ch={int(ch_from_term)} → value={int(value_i32)}\n"
f"[debug] BIN точка: step={int(step)} ch={int(ch_from_term)} → value={float(value_i32):.3f}\n"
)
_dbg_byte_count = parser.bytes_consumed