some kamil_adc fixes

This commit is contained in:
Ayzen
2026-07-02 17:44:24 +03:00
parent 42532c9868
commit 3efe968dd1
10 changed files with 644 additions and 42 deletions
@@ -8,6 +8,7 @@ import unittest
import numpy as np
from python_app.hardware_full.kamil_adc.protocol import (
COMBO_MARKER,
MAIN_MARKER,
REFERENCE_MARKER,
KamilAdcStreamParser,
@@ -18,6 +19,10 @@ def _boundary() -> bytes:
return struct.pack("<HHHH", MAIN_MARKER, 0xFFFF, 0xFFFF, 0xFFFF)
def _combo(input_pos: int, output_pos: int, dirty: int = 0) -> bytes:
return struct.pack("<HHhh", COMBO_MARKER, input_pos, output_pos, dirty)
def _main(step: int, real: int, imag: int) -> bytes:
return struct.pack("<HHhh", MAIN_MARKER, step, real, imag)
@@ -140,6 +145,40 @@ class KamilAdcStreamParserTest(unittest.TestCase):
self.assertEqual(len(sweeps), 1)
self.assertEqual(sweeps[0].main.real.tolist(), [2])
def test_untagged_sweep_has_no_combo(self) -> None:
parser = KamilAdcStreamParser()
(sweep,) = parser.feed(_boundary() + _main(1, 1, 0) + _reference(1, 9, 0) + _boundary())
self.assertIsNone(sweep.combo)
self.assertFalse(sweep.dirty)
def test_combo_tag_labels_following_sweep(self) -> None:
parser = KamilAdcStreamParser()
stream = (
_boundary() + _combo(1, 2)
+ _main(1, 10, 0) + _reference(1, 100, 0)
+ _boundary() + _combo(3, 0, dirty=1)
+ _main(1, 20, 0) + _reference(1, 200, 0)
+ _boundary()
)
first, second = parser.feed(stream)
self.assertEqual(first.combo, (1, 2))
self.assertFalse(first.dirty)
self.assertEqual(second.combo, (3, 0))
self.assertTrue(second.dirty)
def test_combo_not_carried_into_untagged_sweep(self) -> None:
parser = KamilAdcStreamParser()
stream = (
_boundary() + _combo(1, 1)
+ _main(1, 1, 0) + _reference(1, 1, 0)
+ _boundary() # next sweep has no combo frame
+ _main(2, 2, 0) + _reference(2, 2, 0)
+ _boundary()
)
first, second = parser.feed(stream)
self.assertEqual(first.combo, (1, 1))
self.assertIsNone(second.combo)
def test_dtypes(self) -> None:
parser = KamilAdcStreamParser()
(sweep,) = parser.feed(_boundary() + _main(1, 1, 2) + _reference(1, 3, 4) + _boundary())
+99 -1
View File
@@ -16,7 +16,11 @@ import unittest
from unittest import mock
from python_app.hardware_full.kamil_adc import KamilAdcService, KamilAdcTtyReader
from python_app.hardware_full.kamil_adc.protocol import MAIN_MARKER, REFERENCE_MARKER
from python_app.hardware_full.kamil_adc.protocol import (
COMBO_MARKER,
MAIN_MARKER,
REFERENCE_MARKER,
)
from python_app.models.run_config_model import RunConfigModel
from python_app.orchestration.process_supervisor import ProcessSupervisor
@@ -33,6 +37,10 @@ def _reference(step: int, real: int, imag: int) -> bytes:
return struct.pack("<HHhh", REFERENCE_MARKER, step, real, imag)
def _combo(input_pos: int, output_pos: int, dirty: int = 0) -> bytes:
return struct.pack("<HHhh", COMBO_MARKER, input_pos, output_pos, dirty)
class KamilAdcTtyReaderTest(unittest.TestCase):
"""End-to-end tests over a PTY exercising the background reader thread."""
@@ -127,6 +135,41 @@ class KamilAdcTtyReaderTest(unittest.TestCase):
finally:
self._close(master_fd, slave_fd, reader)
def test_read_sweep_for_demuxes_by_combo(self) -> None:
master_fd, slave_fd, reader = self._open_pty_reader()
try:
os.write(
master_fd,
_boundary() + _combo(0, 0) + _main(1, 11, 0) + _reference(1, 1, 0)
+ _boundary() + _combo(0, 1) + _main(1, 22, 0) + _reference(1, 1, 0)
+ _boundary(),
)
# Each combination is served from its own slot, regardless of order.
second = reader.read_sweep_for((0, 1), timeout_s=1.0)
self.assertEqual(second.main.real.tolist(), [22])
self.assertEqual(second.combo, (0, 1))
first = reader.read_sweep_for((0, 0), timeout_s=1.0)
self.assertEqual(first.main.real.tolist(), [11])
finally:
self._close(master_fd, slave_fd, reader)
def test_read_sweep_for_drops_dirty_and_takes_retake(self) -> None:
master_fd, slave_fd, reader = self._open_pty_reader()
try:
os.write(
master_fd,
# A dirty combo (0,1) sweep, then its clean re-take of the same combo.
_boundary() + _combo(0, 1, dirty=1) + _main(1, 99, 0) + _reference(1, 1, 0)
+ _boundary() + _combo(0, 1) + _main(1, 42, 0) + _reference(1, 1, 0)
+ _boundary(),
)
sweep = reader.read_sweep_for((0, 1), timeout_s=1.0)
# The dirty sweep (99) is dropped; only the clean re-take (42) is served.
self.assertEqual(sweep.main.real.tolist(), [42])
self.assertFalse(sweep.dirty)
finally:
self._close(master_fd, slave_fd, reader)
class KamilAdcConfigTest(unittest.TestCase):
def test_config_round_trip_preserves_kamil_sections(self) -> None:
@@ -205,6 +248,61 @@ class KamilAdcConfigTest(unittest.TestCase):
with mock.patch("python_app.hardware_full.kamil_adc.service.os.killpg"):
service.close() # must not raise
def test_drain_after_switch_waits_for_fresh_sweeps(self) -> None:
"""After a switch change, drain must skip the configured number of freshly
published sweeps before returning, so the next capture is post-switch."""
import types
from python_app.hardware_full.kamil_adc import service as service_module
with tempfile.TemporaryDirectory() as tmp_dir:
config = RunConfigModel.from_dict(
{
"radar": {
"model": "kamil_adc",
"driver_mode": "native",
"kamil_adc": {
"project_dir": tmp_dir,
"executable_path": "/bin/sh",
"tty_path": "/tmp/ttyADC_test",
"sweep_timeout_s": 5.0,
},
},
"switches": {"port1": {"positions": 1}, "port2": {"positions": 1}},
}
)
service = KamilAdcService(config)
service._reader = types.SimpleNamespace(published_count=10) # type: ignore[assignment]
service._process = types.SimpleNamespace(poll=lambda: None) # type: ignore[assignment]
# Each poll-sleep advances the published count, as the reader thread would.
def _advance(_seconds: float) -> None:
service._reader.published_count += 1
with mock.patch.object(service_module.time, "sleep", _advance):
service.drain_after_switch(sweeps=3)
# Started at 10, must have waited for at least 3 more sweeps.
self.assertGreaterEqual(service._reader.published_count, 13)
def test_drain_after_switch_is_noop_when_not_open(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
config = RunConfigModel.from_dict(
{
"radar": {
"model": "kamil_adc",
"driver_mode": "native",
"kamil_adc": {
"project_dir": tmp_dir,
"executable_path": "/bin/sh",
"tty_path": "/tmp/ttyADC_test",
},
},
"switches": {"port1": {"positions": 1}, "port2": {"positions": 1}},
}
)
KamilAdcService(config).drain_after_switch() # no reader → must not raise
def test_supervisor_selects_kamil_adc_producer(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
config_path = Path(tmp_dir) / "run_config.json"