kamil_adc support added
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""Tests for Kamil ADC neutral preprocessing-set generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.workflows.kamil_adc_neutral_preprocess import build_kamil_adc_neutral_s21_sets
|
||||
|
||||
|
||||
class KamilAdcNeutralPreprocessTest(unittest.TestCase):
|
||||
def test_builds_passthrough_s21_sets_for_current_sweep(self) -> None:
|
||||
config = RunConfigModel.from_dict(
|
||||
{
|
||||
"radar": {
|
||||
"model": "kamil_adc",
|
||||
"sweep": {
|
||||
"start_hz": 1_000_000.0,
|
||||
"stop_hz": 4_000_000.0,
|
||||
"if_bandwidth_hz": 1.0,
|
||||
"stimulus_power_dbm": -10.0,
|
||||
},
|
||||
},
|
||||
"switches": {
|
||||
"port1": {"positions": 1},
|
||||
"port2": {"positions": 2},
|
||||
},
|
||||
"run": {
|
||||
"combos": [
|
||||
{"input": 0, "output": 0},
|
||||
{"input": 1, "output": 0},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
calibration, reference = build_kamil_adc_neutral_s21_sets(config, point_count=4)
|
||||
|
||||
expected_frequency = np.linspace(1_000_000.0, 4_000_000.0, 4, dtype=np.float32)
|
||||
self.assertEqual(len(calibration.traces), 2)
|
||||
self.assertEqual(len(reference.traces), 2)
|
||||
self.assertEqual(
|
||||
[(trace.combo.input_pos, trace.combo.output_pos) for trace in calibration.traces],
|
||||
[(0, 0), (1, 0)],
|
||||
)
|
||||
for trace in calibration.traces:
|
||||
np.testing.assert_array_equal(trace.frequency_hz, expected_frequency)
|
||||
np.testing.assert_array_equal(trace.s11, np.zeros(4, dtype=np.complex64))
|
||||
np.testing.assert_array_equal(trace.s21, np.ones(4, dtype=np.complex64))
|
||||
for trace in reference.traces:
|
||||
np.testing.assert_array_equal(trace.frequency_hz, expected_frequency)
|
||||
np.testing.assert_array_equal(trace.s11, np.zeros(4, dtype=np.complex64))
|
||||
np.testing.assert_array_equal(trace.s21, np.zeros(4, dtype=np.complex64))
|
||||
|
||||
def test_rejects_non_kamil_config(self) -> None:
|
||||
config = RunConfigModel.from_dict({"radar": {"model": "librevna"}})
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "kamil_adc"):
|
||||
build_kamil_adc_neutral_s21_sets(config, point_count=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -47,9 +47,15 @@ class KamilAdcTtyReaderTest(unittest.TestCase):
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
os.write(master_fd, _start_frame() + _point_frame(1, 10, -1) + _point_frame(2, -20, 2))
|
||||
os.write(
|
||||
master_fd,
|
||||
_start_frame()
|
||||
+ _point_frame(1, 10, -1)
|
||||
+ _point_frame(2, -20, 2)
|
||||
+ _start_frame(),
|
||||
)
|
||||
|
||||
values = reader.read_sweep(points=2, timeout_s=1.0)
|
||||
values = reader.read_sweep(timeout_s=1.0)
|
||||
|
||||
self.assertEqual(values.tolist(), [complex(10, -1), complex(-20, 2)])
|
||||
finally:
|
||||
@@ -58,7 +64,61 @@ class KamilAdcTtyReaderTest(unittest.TestCase):
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
|
||||
def test_short_stream_times_out_with_received_count(self) -> None:
|
||||
def test_stream_reads_consecutive_variable_length_sweeps(self) -> None:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
reader: KamilAdcTtyReader | None = None
|
||||
try:
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
os.write(
|
||||
master_fd,
|
||||
_start_frame()
|
||||
+ _point_frame(1, 10, -1)
|
||||
+ _point_frame(2, -20, 2)
|
||||
+ _start_frame()
|
||||
+ _point_frame(1, 30, -3)
|
||||
+ _start_frame(),
|
||||
)
|
||||
|
||||
first = reader.read_sweep(timeout_s=1.0)
|
||||
second = reader.read_sweep(timeout_s=1.0)
|
||||
|
||||
self.assertEqual(first.tolist(), [complex(10, -1), complex(-20, 2)])
|
||||
self.assertEqual(second.tolist(), [complex(30, -3)])
|
||||
finally:
|
||||
if reader is not None:
|
||||
reader.close()
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
|
||||
def test_expected_point_count_discards_mismatched_sweep(self) -> None:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
reader: KamilAdcTtyReader | None = None
|
||||
try:
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
os.write(
|
||||
master_fd,
|
||||
_start_frame()
|
||||
+ _point_frame(1, 5, -5)
|
||||
+ _start_frame()
|
||||
+ _point_frame(1, 10, -1)
|
||||
+ _point_frame(2, -20, 2)
|
||||
+ _start_frame(),
|
||||
)
|
||||
|
||||
values = reader.read_sweep(timeout_s=1.0, expected_points=2)
|
||||
|
||||
self.assertEqual(values.tolist(), [complex(10, -1), complex(-20, 2)])
|
||||
finally:
|
||||
if reader is not None:
|
||||
reader.close()
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
|
||||
def test_stream_without_next_start_times_out_with_received_count(self) -> None:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
reader: KamilAdcTtyReader | None = None
|
||||
try:
|
||||
@@ -67,8 +127,8 @@ class KamilAdcTtyReaderTest(unittest.TestCase):
|
||||
reader.open()
|
||||
os.write(master_fd, _start_frame() + _point_frame(1, 10, -1))
|
||||
|
||||
with self.assertRaisesRegex(TimeoutError, "received 1/2"):
|
||||
reader.read_sweep(points=2, timeout_s=0.05)
|
||||
with self.assertRaisesRegex(TimeoutError, "sweep end: received 1 points"):
|
||||
reader.read_sweep(timeout_s=0.05)
|
||||
finally:
|
||||
if reader is not None:
|
||||
reader.close()
|
||||
@@ -137,6 +197,7 @@ class KamilAdcConfigTest(unittest.TestCase):
|
||||
encoded = RunConfigModel.from_dict(payload).to_dict()
|
||||
|
||||
self.assertEqual(encoded["radar"]["model"], "kamil_adc")
|
||||
self.assertNotIn("points", encoded["radar"]["sweep"])
|
||||
self.assertEqual(encoded["radar"]["kamil_adc"]["tty_path"], "/tmp/ttyADC_data")
|
||||
self.assertEqual(encoded["radar"]["kamil_adc"]["args"], ["profile:phase", "do1_pair_subtract_avg"])
|
||||
self.assertEqual(encoded["radar"]["kamil_adc"]["env"], {"ADC_ENV": "1"})
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Laser-control protocol compatibility tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from python_app.hardware_full.laser_control.controller import DEVICE_MAIN_MESSAGE_ID, LaserController
|
||||
from python_app.hardware_full.laser_control.models import VariationType
|
||||
from python_app.hardware_full.laser_control.protocol import Protocol, TaskType
|
||||
from python_app.hardware_full.kamil_adc_service import apply_kamil_adc_laser_control
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
|
||||
|
||||
DEVICE_MAIN_MANUAL_HEX = "1111ff37ffa518ab000000000000000a8000000a8000ff003d2acc2c163f"
|
||||
DEVICE_MAIN_CHANGE_CURRENT_LD1_HEX = (
|
||||
"7777ff3701003d2acc2c10008813ffa5cc2c18ab0a00000a8000000a8000b600"
|
||||
)
|
||||
DEVICE_MAIN_CHANGE_CURRENT_LD2_HEX = (
|
||||
"7777ff3702003d2acc2c0500881318ab3d2affa50a00000a8000000a80005106"
|
||||
)
|
||||
|
||||
|
||||
class _FakeProtocol:
|
||||
def __init__(self) -> None:
|
||||
self.is_connected = True
|
||||
self.sent: list[bytes] = []
|
||||
|
||||
def send_raw(self, data: bytes) -> None:
|
||||
self.sent.append(bytes(data))
|
||||
|
||||
def receive_raw(self, length: int) -> bytes:
|
||||
return b"\x00\x00" if length == 2 else b""
|
||||
|
||||
|
||||
class _FakeLaserController:
|
||||
instances: list["_FakeLaserController"] = []
|
||||
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
self.kwargs = kwargs
|
||||
self.calls: list[tuple[str, object]] = []
|
||||
_FakeLaserController.instances.append(self)
|
||||
|
||||
def connect(self) -> bool:
|
||||
self.calls.append(("connect", None))
|
||||
return True
|
||||
|
||||
def reset(self) -> None:
|
||||
self.calls.append(("reset", None))
|
||||
|
||||
def set_manual_mode(self, **kwargs: object) -> None:
|
||||
self.calls.append(("set_manual_mode", kwargs))
|
||||
|
||||
def start_variation(self, **kwargs: object) -> None:
|
||||
self.calls.append(("start_variation", kwargs))
|
||||
|
||||
def disconnect(self) -> None:
|
||||
self.calls.append(("disconnect", None))
|
||||
|
||||
|
||||
class LaserControlProtocolCompatibilityTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
_FakeLaserController.instances.clear()
|
||||
|
||||
@staticmethod
|
||||
def _kamil_config(laser_payload: dict[str, object]) -> RunConfigModel:
|
||||
return RunConfigModel.from_dict(
|
||||
{
|
||||
"radar": {
|
||||
"model": "kamil_adc",
|
||||
"driver_mode": "native",
|
||||
"laser_control": laser_payload,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def test_manual_command_matches_device_main_bytes(self) -> None:
|
||||
command = Protocol.encode_decode_enable(
|
||||
temp1=28.0,
|
||||
temp2=28.9,
|
||||
current1=33.0,
|
||||
current2=35.0,
|
||||
pi_coeff1_p=2560,
|
||||
pi_coeff1_i=128,
|
||||
pi_coeff2_p=2560,
|
||||
pi_coeff2_i=128,
|
||||
message_id=DEVICE_MAIN_MESSAGE_ID,
|
||||
)
|
||||
|
||||
self.assertEqual(command.hex(), DEVICE_MAIN_MANUAL_HEX)
|
||||
|
||||
def test_variation_commands_match_device_main_bytes(self) -> None:
|
||||
ld1_command = Protocol.encode_task_enable(
|
||||
task_type=TaskType.CHANGE_CURRENT_LD1,
|
||||
static_temp1=28.0,
|
||||
static_temp2=28.9,
|
||||
static_current1=33.0,
|
||||
static_current2=35.0,
|
||||
min_value=33.0,
|
||||
max_value=35.0,
|
||||
step=0.05,
|
||||
time_step=50,
|
||||
delay_time=10,
|
||||
message_id=DEVICE_MAIN_MESSAGE_ID,
|
||||
pi_coeff1_p=2560,
|
||||
pi_coeff1_i=128,
|
||||
pi_coeff2_p=2560,
|
||||
pi_coeff2_i=128,
|
||||
)
|
||||
ld2_command = Protocol.encode_task_enable(
|
||||
task_type=TaskType.CHANGE_CURRENT_LD2,
|
||||
static_temp1=28.0,
|
||||
static_temp2=28.9,
|
||||
static_current1=33.0,
|
||||
static_current2=35.0,
|
||||
min_value=33.0,
|
||||
max_value=35.0,
|
||||
step=0.05,
|
||||
time_step=50,
|
||||
delay_time=10,
|
||||
message_id=DEVICE_MAIN_MESSAGE_ID,
|
||||
pi_coeff1_p=2560,
|
||||
pi_coeff1_i=128,
|
||||
pi_coeff2_p=2560,
|
||||
pi_coeff2_i=128,
|
||||
)
|
||||
|
||||
self.assertEqual(ld1_command.hex(), DEVICE_MAIN_CHANGE_CURRENT_LD1_HEX)
|
||||
self.assertEqual(ld2_command.hex(), DEVICE_MAIN_CHANGE_CURRENT_LD2_HEX)
|
||||
|
||||
def test_start_sequence_matches_device_main_order(self) -> None:
|
||||
fake_protocol = _FakeProtocol()
|
||||
controller = LaserController(pi_coeff1_p=2560, pi_coeff1_i=128, pi_coeff2_p=2560, pi_coeff2_i=128)
|
||||
controller._protocol = fake_protocol
|
||||
|
||||
with patch("python_app.hardware_full.laser_control.controller.time.sleep", return_value=None):
|
||||
controller.reset()
|
||||
controller.set_manual_mode(
|
||||
temp1=28.0,
|
||||
temp2=28.9,
|
||||
current1=33.0,
|
||||
current2=35.0,
|
||||
message_id=DEVICE_MAIN_MESSAGE_ID,
|
||||
)
|
||||
controller.start_variation(
|
||||
variation_type=VariationType.CHANGE_CURRENT_LD1,
|
||||
params={
|
||||
"static_temp1": 28.0,
|
||||
"static_temp2": 28.9,
|
||||
"static_current1": 33.0,
|
||||
"static_current2": 35.0,
|
||||
"min_value": 33.0,
|
||||
"max_value": 35.0,
|
||||
"step": 0.05,
|
||||
"time_step": 50,
|
||||
"delay_time": 10,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[command.hex() for command in fake_protocol.sent],
|
||||
[
|
||||
"2222",
|
||||
DEVICE_MAIN_MANUAL_HEX,
|
||||
DEVICE_MAIN_CHANGE_CURRENT_LD1_HEX,
|
||||
],
|
||||
)
|
||||
|
||||
def test_stop_sequence_restores_device_main_manual_bytes(self) -> None:
|
||||
fake_protocol = _FakeProtocol()
|
||||
controller = LaserController(pi_coeff1_p=2560, pi_coeff1_i=128, pi_coeff2_p=2560, pi_coeff2_i=128)
|
||||
controller._protocol = fake_protocol
|
||||
|
||||
with patch("python_app.hardware_full.laser_control.controller.time.sleep", return_value=None):
|
||||
controller.set_manual_mode(
|
||||
temp1=28.0,
|
||||
temp2=28.9,
|
||||
current1=33.0,
|
||||
current2=35.0,
|
||||
message_id=DEVICE_MAIN_MESSAGE_ID,
|
||||
)
|
||||
fake_protocol.sent.clear()
|
||||
controller.stop_task(restore_message_id=DEVICE_MAIN_MESSAGE_ID)
|
||||
|
||||
self.assertEqual(
|
||||
[command.hex() for command in fake_protocol.sent],
|
||||
[
|
||||
"2222",
|
||||
DEVICE_MAIN_MANUAL_HEX,
|
||||
],
|
||||
)
|
||||
|
||||
def test_apply_radar_variation_sequence_matches_device_main_order(self) -> None:
|
||||
config = self._kamil_config(
|
||||
{
|
||||
"enabled": True,
|
||||
"port": "/dev/ttyUSB0",
|
||||
"mode": "variation",
|
||||
"pi_coeff1_p": 2560,
|
||||
"pi_coeff1_i": 128,
|
||||
"pi_coeff2_p": 2560,
|
||||
"pi_coeff2_i": 128,
|
||||
"variation": {
|
||||
"variation_type": "CHANGE_CURRENT_LD1",
|
||||
"static_temp1": 28.0,
|
||||
"static_temp2": 28.9,
|
||||
"static_current1": 33.0,
|
||||
"static_current2": 35.0,
|
||||
"min_value": 33.0,
|
||||
"max_value": 35.0,
|
||||
"step": 0.05,
|
||||
"time_step": 50,
|
||||
"delay_time": 10,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
with patch("python_app.hardware_full.laser_control.controller.LaserController", _FakeLaserController):
|
||||
applied = apply_kamil_adc_laser_control(config)
|
||||
|
||||
self.assertTrue(applied)
|
||||
controller = _FakeLaserController.instances[0]
|
||||
self.assertEqual(
|
||||
[name for name, _payload in controller.calls],
|
||||
["connect", "reset", "set_manual_mode", "start_variation", "disconnect"],
|
||||
)
|
||||
manual_payload = controller.calls[2][1]
|
||||
self.assertEqual(
|
||||
manual_payload,
|
||||
{
|
||||
"temp1": 28.0,
|
||||
"temp2": 28.9,
|
||||
"current1": 33.0,
|
||||
"current2": 35.0,
|
||||
"message_id": DEVICE_MAIN_MESSAGE_ID,
|
||||
},
|
||||
)
|
||||
|
||||
def test_apply_radar_skips_disabled_laser_control(self) -> None:
|
||||
config = self._kamil_config({"enabled": False})
|
||||
|
||||
with patch("python_app.hardware_full.laser_control.controller.LaserController", _FakeLaserController):
|
||||
applied = apply_kamil_adc_laser_control(config)
|
||||
|
||||
self.assertFalse(applied)
|
||||
self.assertEqual(_FakeLaserController.instances, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user