new mode --bin24

This commit is contained in:
awe
2026-08-17 14:06:49 +03:00
parent 2cf7543bbe
commit 428ba2a9e0
6 changed files with 392 additions and 14 deletions
+21
View File
@@ -13,6 +13,7 @@ from rfg_adc_plotter.gui.pyqtgraph_backend import (
build_logdet_voltage_fft_input,
build_main_window_layout,
coalesce_packets_for_ui,
TTY_CODE_SCALE_DENOM_24,
compute_background_subtracted_bscan_levels,
compute_aux_phase_curve,
compute_do1_tagged_aggregate,
@@ -87,6 +88,26 @@ class ProcessingTests(unittest.TestCase):
self.assertTrue(np.all(volts >= -5.0))
self.assertTrue(np.all(volts <= 5.0))
def test_convert_tty_i16_to_voltage_int24_denominator_maps_full_scale(self):
full_scale = float(TTY_CODE_SCALE_DENOM_24)
codes = np.asarray([-full_scale - 1.0, 0.0, full_scale], dtype=np.float32)
volts = convert_tty_i16_to_voltage(codes, 5.0, denom=TTY_CODE_SCALE_DENOM_24)
self.assertAlmostEqual(float(volts[0]), -5.0, places=4)
self.assertAlmostEqual(float(volts[1]), 0.0, places=6)
self.assertAlmostEqual(float(volts[2]), 5.0, places=4)
# A quarter-scale 24-bit code must yield ~1.25 V with the int24 denom, but the
# default int16 denom over-scales it ~256x and clips to the ±5 V range. This
# guards that --bin24 uses the wider full-scale rather than the int16 one.
quarter = np.asarray([full_scale / 4.0], dtype=np.float32)
self.assertAlmostEqual(
float(convert_tty_i16_to_voltage(quarter, 5.0, denom=TTY_CODE_SCALE_DENOM_24)[0]),
1.25,
places=4,
)
self.assertAlmostEqual(float(convert_tty_i16_to_voltage(quarter, 5.0)[0]), 5.0, places=6)
def test_build_logdet_voltage_fft_input_converts_codes_and_exponentiates(self):
codes = np.asarray([-32768.0, 0.0, 32767.0], dtype=np.float32)
volts, fft_input = build_logdet_voltage_fft_input(codes, 5.0)
+180
View File
@@ -10,6 +10,7 @@ from rfg_adc_plotter.io.sweep_parser_core import (
BatchPointEvent,
ComplexAsciiSweepParser,
LegacyBinaryParser,
LegacyBinaryParser24,
LogScale16BitX2BinaryParser,
LogScaleBinaryParser32,
ParserTestStreamParser,
@@ -17,6 +18,7 @@ from rfg_adc_plotter.io.sweep_parser_core import (
StartEvent,
SweepAssembler,
log_pair_to_sweep,
u24_to_i24,
)
@@ -120,6 +122,35 @@ def _pack_logdet_point(step: int, value: int) -> bytes:
)
def _i24le(value: int) -> bytes:
v = int(value) & 0xFFFFFF
return bytes((v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF))
def _pack_tty24_start() -> bytes:
return _u16le(0x000A) + b"\xff" * 8
def _pack_tty24_point(step: int, ch1: int, ch2: int) -> bytes:
return _u16le(0x000A) + _u16le(step) + _i24le(ch1) + _i24le(ch2)
def _pack_tty24_tagged_point(marker_word0: int, step: int, ch1: int, ch2: int) -> bytes:
return _u16le(marker_word0) + _u16le(step) + _i24le(ch1) + _i24le(ch2)
def _pack_tty24_tagged_low_point(step: int, ch1: int, ch2: int) -> bytes:
return _pack_tty24_tagged_point(0x00A3, step, ch1, ch2)
def _pack_tty24_tagged_high_point(step: int, ch1: int, ch2: int) -> bytes:
return _pack_tty24_tagged_point(0x00A4, step, ch1, ch2)
def _pack_tty24_secondary_point(step: int, ch1: int, ch2: int) -> bytes:
return _u16le(0x00A8) + _u16le(step) + _i24le(ch1) + _i24le(ch2)
class SweepParserCoreTests(unittest.TestCase):
def test_ascii_parser_emits_start_and_points(self):
parser = AsciiSweepParser()
@@ -697,5 +728,154 @@ class SweepParserCoreTests(unittest.TestCase):
self.assertNotIn("_secondary_payload", info)
class LegacyBinaryParser24Tests(unittest.TestCase):
def test_u24_to_i24_sign_extension_boundaries(self):
self.assertEqual(u24_to_i24(0x000000), 0)
self.assertEqual(u24_to_i24(0x000001), 1)
self.assertEqual(u24_to_i24(0x7FFFFF), 8388607)
self.assertEqual(u24_to_i24(0x800000), -8388608)
self.assertEqual(u24_to_i24(0xFFFFFF), -1)
def test_accepts_tty24_ch1_ch2_stream(self):
parser = LegacyBinaryParser24()
stream = b"".join(
[
_pack_tty24_start(),
_pack_tty24_point(1, 100, 90),
_pack_tty24_point(2, 120, 95),
]
)
events = parser.feed(stream)
self.assertIsInstance(events[0], StartEvent)
self.assertEqual(events[0].ch, 0)
self.assertIsInstance(events[1], PointEvent)
self.assertEqual(events[1].x, 1)
self.assertEqual(events[1].y, 18100.0)
self.assertEqual(events[1].aux, (100.0, 90.0))
self.assertEqual(events[1].signal_kind, "bin_iq")
self.assertEqual(events[2].x, 2)
self.assertEqual(events[2].y, 23425.0)
self.assertEqual(events[2].aux, (120.0, 95.0))
def test_decodes_negative_int24_values(self):
parser = LegacyBinaryParser24()
stream = _pack_tty24_start() + _pack_tty24_point(1, -8388608, 8388607)
events = parser.feed(stream)
point = events[1]
self.assertEqual(point.aux, (-8388608.0, 8388607.0))
self.assertEqual(point.y, float(8388608 ** 2 + 8388607 ** 2))
def test_never_emits_batch_event(self):
parser = LegacyBinaryParser24()
stream = _pack_tty24_start() + b"".join(
_pack_tty24_point(i, i * 10, -i * 5) for i in range(1, 6)
)
events = parser.feed(stream)
self.assertFalse(any(isinstance(e, BatchPointEvent) for e in events))
self.assertFalse(parser._try_emit_tty_batch(events, require_not_legacy=False))
def test_resynchronizes_after_garbage(self):
parser = LegacyBinaryParser24()
stream = b"\x11\x22\x33" + _pack_tty24_start() + _pack_tty24_point(5, 7, -3)
events = parser.feed(stream)
points = [e for e in events if isinstance(e, PointEvent)]
self.assertEqual(len(points), 1)
self.assertEqual(points[0].x, 5)
self.assertEqual(points[0].aux, (7.0, -3.0))
def test_detects_new_sweep_on_step_reset(self):
parser = LegacyBinaryParser24()
stream = _pack_tty24_start() + b"".join(
[
_pack_tty24_point(5, 1, 1),
_pack_tty24_point(6, 2, 2),
_pack_tty24_point(1, 3, 3),
]
)
events = parser.feed(stream)
start_count = sum(1 for e in events if isinstance(e, StartEvent))
self.assertEqual(start_count, 2)
def test_do1_tagged_routing(self):
parser = LegacyBinaryParser24()
stream = b"".join(
[
_pack_tty24_tagged_low_point(1, 10, 20),
_pack_tty24_tagged_high_point(1, 30, 40),
]
)
events = parser.feed(stream)
points = [e for e in events if isinstance(e, PointEvent)]
self.assertEqual(points[0].signal_kind, "bin_iq_do1_tagged")
self.assertEqual(points[0].do1_level, "low")
self.assertEqual(points[0].aux, (10.0, 20.0))
self.assertEqual(points[1].do1_level, "high")
self.assertEqual(points[1].aux, (30.0, 40.0))
def test_secondary_point(self):
parser = LegacyBinaryParser24()
stream = _pack_tty24_start() + _pack_tty24_secondary_point(3, -51, -36)
events = parser.feed(stream)
secondary = [e for e in events if isinstance(e, PointEvent) and e.is_secondary]
self.assertEqual(len(secondary), 1)
self.assertEqual(secondary[0].x, 3)
self.assertEqual(secondary[0].y, 0.0)
self.assertEqual(secondary[0].aux, (-51.0, -36.0))
def test_assembles_sweep_packet(self):
parser = LegacyBinaryParser24()
assembler = SweepAssembler(fancy=False, apply_inversion=True)
stream = _pack_tty24_start() + b"".join(
_pack_tty24_point(i, i * 10, -i * 5) for i in range(1, 20)
)
packets = []
for event in parser.feed(stream):
packet = assembler.consume(event)
if packet is not None:
packets.append(packet)
final = assembler.finalize_current()
if final is not None:
packets.append(final)
self.assertTrue(packets)
sweep, info, _aux = packets[-1]
self.assertEqual(info["signal_kind"], "bin_iq")
self.assertTrue(np.isfinite(info["mean"]))
class SweepReaderBin24Tests(unittest.TestCase):
def test_build_parser_selects_int24_parser(self):
from queue import Queue
import threading
from rfg_adc_plotter.io.sweep_reader import SweepReader
reader = SweepReader(
"unused",
115200,
Queue(),
threading.Event(),
bin24_mode=True,
)
parser, _assembler = reader._build_parser()
self.assertIsInstance(parser, LegacyBinaryParser24)
self.assertEqual(reader._resolve_parser_mode_label(), "legacy_10byte_i24")
if __name__ == "__main__":
unittest.main()