something working new format
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user