From 6d0b7b4e1420319d0b96461f56d5a6fc2c039f27 Mon Sep 17 00:00:00 2001 From: Hoca Chen Date: Sat, 8 Mar 2025 13:45:06 +0800 Subject: [PATCH 1/5] add the remote command for Probe group as title --- .../Sentio/CommandGroups/ProbeCommandGroup.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py index d2e3296..734cf81 100644 --- a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py @@ -321,3 +321,47 @@ def step_probe_site_next(self, probe: ProbeSentio) -> Tuple[str, float, float]: resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") return tok[0], float(tok[1]), float(tok[2]) + + def enable_probe_motor(self, probe: ProbeSentio, status: bool) -> None: + """Enable/Disable the probe motor. + + Probe with 3 motors will enable and disable by following behavior. + + Args: + probe: The probe to action. + status: Enable or disable status + + """ + self.comm.send(f"enable_positioner_motor {probe.toSentioAbbr()},{status}") + Response.check_resp(self.comm.read_line()) + + def get_probe_status(self, probe: ProbeSentio) -> str: + """Obtain the status of probe. + + Command will return 4 probe status + + Args: + probe: The probe to step. + + Returns: + Status of positioner with 4 digits, 1st digit indicates the East Positioner, 2nd digit indicates West Positioner + 3rd digit indicates the North Positioner, 4rd digit indicates the South Positioner. + """ + + self.comm.send(f"get_positioner_status {probe.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def set_probe_status(self, probe: ProbeSentio, status: bool) -> None: + """Enable/Disable the probe stage in the SENTIO. + + Enable/Disable the Probes in the SENTIO. + + Args: + probe: The probe to action. + status: Enable or disable status + + """ + self.comm.send(f"set_positioner_status {probe.toSentioAbbr()},{status}") + Response.check_resp(self.comm.read_line()) + From f86bedfd50261d5045c4366e330ca493162b0dd4 Mon Sep 17 00:00:00 2001 From: Hoca Chen Date: Sun, 9 Mar 2025 09:16:25 +0800 Subject: [PATCH 2/5] fixed the command is inconsistence by document as title --- .../Sentio/CommandGroups/ProbeCommandGroup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py index 734cf81..b7e0858 100644 --- a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py @@ -83,7 +83,7 @@ def async_step_probe_site_first(self, probe: ProbeSentio) -> int: return resp.cmd_id() - def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[int, str, float, float]: + def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[int, float, float, str]: """Get information for a probe site. Each positioner can define n a number of predefined positions called "sites". @@ -93,13 +93,13 @@ def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[int, str, float, probe: The probe to get the site for. Returns: - A tuple containing the site index, the site name, the x position in micrometer and the y position in micrometer. + A tuple containing the site index, position x, the position y in micrometer and the reference. """ self.comm.send(f"get_positioner_site {probe.toSentioAbbr()},{idx}") resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") - return int(tok[0]), str(tok[1]), float(tok[2]), float(tok[3]) + return int(tok[0]), float(tok[1]), float(tok[2]), str(tok[3]) def get_probe_site_number(self, probe: ProbeSentio) -> int: @@ -144,7 +144,7 @@ def get_probe_z(self, probe: ProbeSentio, ref: ProbeZReference) -> float: Returns: The z position in micrometer. """ - self.comm.send(f"get_positioner_z {probe.toSentioAbbr()}, {ref.toSentioAbbr()}") + self.comm.send(f"get_positioner_z {probe.toSentioAbbr()},{ref.toSentioAbbr()}") resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) @@ -259,7 +259,7 @@ def set_probe_home(self, probe: ProbeSentio, site: ChuckSite | None = None, x: f if site is None: self.comm.send(f"set_positioner_home {probe.toSentioAbbr()}") else: - self.comm.send(f"set_positioner_home {probe.toSentioAbbr()}, {site.toSentioAbbr()}, {x}, {y}") + self.comm.send(f"set_positioner_home {probe.toSentioAbbr()},{site.toSentioAbbr()},{x},{y}") Response.check_resp(self.comm.read_line()) From 069dbbf9f8b93d23318a0fbca0b131c90c27d7f5 Mon Sep 17 00:00:00 2001 From: Hoca Chen Date: Sat, 15 Mar 2025 08:40:11 +0800 Subject: [PATCH 3/5] add SiPH group commands --- .../Sentio/CommandGroups/SiPHCommandGroup.py | 219 +++++++++++++++++- sentio_prober_control/Sentio/Enumerations.py | 42 ++++ 2 files changed, 260 insertions(+), 1 deletion(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py index 464e969..66a8e20 100644 --- a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py @@ -1,6 +1,6 @@ from typing import Tuple -from sentio_prober_control.Sentio.Enumerations import ProbeSentio +from sentio_prober_control.Sentio.Enumerations import ProbeSentio, UvwAxis, FiberType from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase @@ -77,3 +77,220 @@ def move_separation(self, probe: ProbeSentio) -> None: self.comm.send(f"siph:move_separation {probe.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) + + def coupling(self, probe: ProbeSentio, axis: UvwAxis) -> None: + """Start execute coupling . + + Args: + probe: Execute probe. + axis: Execute axis + """ + + self.comm.send(f"siph:coupling {probe.toSentioAbbr()},{axis.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def get_alignment(self, probe: ProbeSentio, fiber_type: FiberType) -> Tuple[bool, bool, bool, bool]: + """Get the fast alignment function enable including Coarse, Fine, Gradient, and Rotary/Focal searching. + + Args: + probe: The probe to get the alignment settings for. + fiber_type: The type of fiber used (Single, Array, or Lensed). + + Returns: + A tuple containing the status of Coarse, Fine, Gradient, and Rotary/Focal searching (True/False). + """ + self.comm.send(f"siph:get_alignment {probe.toSentioAbbr()},{fiber_type}") + resp = Response.check_resp(self.comm.read_line()) + + tok = resp.message().split(",") + coarse = tok[0].strip().lower() == "true" + fine = tok[1].strip().lower() == "true" + gradient = tok[2].strip().lower() == "true" + rotary_focal = tok[3].strip().lower() == "true" + + return coarse, fine, gradient, rotary_focal + + def set_origin(self, probe: ProbeSentio) -> None: + """Set the current position as the origin position for the SiPH positioner. + + Args: + probe: The probe to set the origin position for (East or West). + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:set_origin {probe.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def move_origin(self, probe: ProbeSentio) -> None: + """Move SiPH positioner to its origin position. + + The movement includes: + - NanoCube XY moves back to 50 μm. + - UVW axes move back to the position set during Hover Height training. + - If the axis is in "Manual" mode, only NanoCube moves to 50 μm. + + Args: + probe: The probe to move to origin position (East or West). + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:move_origin {probe.toSentioAbbr()}") + return Response.check_resp(self.comm.read_line()) + + def move_position_uvw(self, probe: ProbeSentio, axis: UvwAxis, degree: float) -> float: + """Move the SiPH positioner target axis with a relative degree. + + Args: + probe: The positioner ID to move (East or West). + axis: The axis to move (U, V, or W). + degree: The relative degree to move. + + Returns: + The current position of the axis after movement. + """ + self.comm.send(f"siph:move_position_uvw {probe.toSentioAbbr()},{axis.toSentioAbbr()},{degree}") + resp = Response.check_resp(self.comm.read_line()) + + return float(resp.message()) + + def pivot_point(self, probe: ProbeSentio) -> None: + """Run pivot point calibration for the specified positioner. + + Args: + probe: The positioner ID to calibrate (East or West). + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:pivot_point {probe.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_alignment(self, probe: ProbeSentio, fiber_type: FiberType, coarse: bool, fine: bool, gradient: bool, + rotary: bool) -> None: + """Set the fast alignment function enable including Coarse, Fine, Gradient, and Rotary/Focal searching. + + Args: + probe: The positioner ID to calibrate (East or West). + fiber_type: The fiber type ("Single", "Array", "Lensed"). + coarse: Enable or disable coarse search (True = ON, False = OFF). + fine: Enable or disable fine search (True = ON, False = OFF). + gradient: Enable or disable gradient search (True = ON, False = OFF). + rotary: Enable or disable rotary/focal search (True = ON, False = OFF, not supported for "Single" fiber type). + + Returns: + A Response object containing the command execution status. + """ + coarse_str = "ON" if coarse else "OFF" + fine_str = "ON" if fine else "OFF" + gradient_str = "ON" if gradient else "OFF" + rotary_str = "ON" if rotary else "OFF" + + self.comm.send( + f"siph:set_alignment {probe.toSentioAbbr()},{fiber_type},{coarse_str},{fine_str},{gradient_str},{rotary_str}") + Response.check_resp(self.comm.read_line()) + + def set_pivot_point(self, rotary_angle_1: float, rotary_angle_2: float, leveling_angle: float, repeats: int) -> None: + """Set the parameters for the pivot point function. + + Args: + rotary_angle_1: The first rotary angle. + rotary_angle_2: The second rotary angle. + leveling_angle: The leveling angle. + repeats: The number of repetitions. + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:set_pivot_point {rotary_angle_1},{rotary_angle_2},{leveling_angle},{repeats}") + Response.check_resp(self.comm.read_line()) + + def download_graph_data(self, file_path: str, file_name: str) -> None: + """Download the graph data and save it to the specified location. + + Args: + file_path: The directory path where the graph data will be saved. + file_name: The name of the file to save the graph data. + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:download_graph_data {file_path}, {file_name}") + Response.check_resp(self.comm.read_line()) + + def start_tracking(self, timeout: int = 60) -> int: + """Start the SiPH positioner gradient tracking search asynchronously. + + Args: + timeout: Timeout value in seconds (range: 1~600). Default is 60 sec. + + Returns: + The asynchronous command ID, which can be used to check status or abort. + """ + self.comm.send(f"siph:start_tracking {timeout}") + resp = Response.check_resp(self.comm.read_line()) + + # Extract asynchronous command ID from response + command_id = int(resp.message().split(",")[1]) + return command_id + + def move_nanocube_xy(self, probe: ProbeSentio, x: float, y: float) -> tuple: + """Move NanoCube to the target XY position. + + The movement range is limited to 0 ~ 100 μm. + + Args: + probe: The positioner (East or West). + x: Target X position (μm), must be in range [0, 100]. + y: Target Y position (μm), must be in range [0, 100]. + + Returns: + A tuple containing the new X and Y positions after movement. + """ + if not (0 <= x <= 100 and 0 <= y <= 100): + raise ValueError("X and Y values must be between 0 and 100 μm.") + + self.comm.send(f"siph:move_nanocube_xy {probe.toSentioAbbr()},{x},{y}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + new_x = float(tok[1]) + new_y = float(tok[2]) + return new_x, new_y + + def get_nanocube_xy(self, probe: ProbeSentio) -> tuple: + """Get the current NanoCube XY position. + + Args: + probe: The positioner (East or West). + + Returns: + A tuple containing the current X and Y positions. + """ + self.comm.send(f"siph:get_nanocube_xy {probe.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + current_x = float(tok[1]) + current_y = float(tok[2]) + return current_x, current_y + + def get_nanocube_z(self, probe: ProbeSentio) -> float: + """Get the current NanoCube Z position. + + Args: + probe: The positioner (East or West). + + Returns: + The current Z position. + """ + self.comm.send(f"siph:get_nanocube_z {probe.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + current_z = float(tok[1]) + return current_z \ No newline at end of file diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 97e56d9..1974945 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -1550,3 +1550,45 @@ def toSentioAbbr(self): ZPositionHint.Transfer: "Transfer", } return switcher.get(self, "Invalid ZPositionHint") + +class UvwAxis(Enum): + """An enumeration containing UVW axis. + + Attributes: + U (0): U axis. + V (1): V axis. + W (2): W axis. + """ + + U = 0 + V = 1 + W = 2 + + def toSentioAbbr(self): + switcher = { + UvwAxis.U: "U", + UvwAxis.V: "V", + UvwAxis.W: "W", + } + return switcher.get(self, "Invalid UVW enumerator") + +class FiberType(Enum): + """An enumeration containing supported fiber type. + + Attributes: + Single (0) + Array (1) + Lensed (2) + """ + + Single = 0 + Array = 1 + Lensed = 2 + + def toSentioAbbr(self): + switcher = { + FiberType.Single: "Single", + FiberType.Array: "Array", + FiberType.Lensed: "Lensed", + } + return switcher.get(self, "Invalid fiber type enumerator") From 4a39645e772a9b2e2116d36e634b0d31799c4ca7 Mon Sep 17 00:00:00 2001 From: Hoca Chen Date: Sat, 15 Mar 2025 10:43:11 +0800 Subject: [PATCH 4/5] add unit test for all the SiPH command group and probe --- .../Sentio/CommandGroups/ProbeCommandGroup.py | 4 +- .../Sentio/CommandGroups/SiPHCommandGroup.py | 22 ++-- .../UnitTest/TestProbeCommandGroup.py | 14 +- .../UnitTest/TestSiPHCommanfGroup.py | 122 ++++++++++++++++++ 4 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 sentio_prober_control/UnitTest/TestSiPHCommanfGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py index b7e0858..dd81592 100644 --- a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py @@ -99,7 +99,7 @@ def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[int, float, floa resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") - return int(tok[0]), float(tok[1]), float(tok[2]), str(tok[3]) + return str(tok[0]), float(tok[1]), float(tok[2]), str(tok[3]) def get_probe_site_number(self, probe: ProbeSentio) -> int: @@ -226,7 +226,7 @@ def move_probe_z(self, probe: ProbeSentio, ref: ProbeZReference, z: float) -> fl The z position after the move in micrometer (from zero). """ - self.comm.send(f"move_positioner_z {probe.toSentioAbbr()}, {ref.toSentioAbbr()}, {z}") + self.comm.send(f"move_positioner_z {probe.toSentioAbbr()},{ref.toSentioAbbr()},{z}") resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) diff --git a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py index 66a8e20..2df8301 100644 --- a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py @@ -15,11 +15,11 @@ def __init__(self, comm) -> None: super().__init__(comm) - def fast_alignment(self) -> Response: + def fast_alignment(self) -> None: """Perform fast fiber alignment.""" self.comm.send("siph:fast_alignment") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def get_cap_sensor(self) -> Tuple[float, float]: @@ -99,7 +99,7 @@ def get_alignment(self, probe: ProbeSentio, fiber_type: FiberType) -> Tuple[bool Returns: A tuple containing the status of Coarse, Fine, Gradient, and Rotary/Focal searching (True/False). """ - self.comm.send(f"siph:get_alignment {probe.toSentioAbbr()},{fiber_type}") + self.comm.send(f"siph:get_alignment {probe.toSentioAbbr()},{fiber_type.toSentioAbbr()}") resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") @@ -232,10 +232,10 @@ def start_tracking(self, timeout: int = 60) -> int: resp = Response.check_resp(self.comm.read_line()) # Extract asynchronous command ID from response - command_id = int(resp.message().split(",")[1]) + command_id = int(resp.cmd_id()) return command_id - def move_nanocube_xy(self, probe: ProbeSentio, x: float, y: float) -> tuple: + def move_nanocube_xy(self, probe: ProbeSentio, x: float, y: float) -> tuple[float, float]: """Move NanoCube to the target XY position. The movement range is limited to 0 ~ 100 μm. @@ -256,11 +256,11 @@ def move_nanocube_xy(self, probe: ProbeSentio, x: float, y: float) -> tuple: # Parse response message tok = resp.message().split(",") - new_x = float(tok[1]) - new_y = float(tok[2]) + new_x = float(tok[0]) + new_y = float(tok[1]) return new_x, new_y - def get_nanocube_xy(self, probe: ProbeSentio) -> tuple: + def get_nanocube_xy(self, probe: ProbeSentio) -> tuple[float, float]: """Get the current NanoCube XY position. Args: @@ -274,8 +274,8 @@ def get_nanocube_xy(self, probe: ProbeSentio) -> tuple: # Parse response message tok = resp.message().split(",") - current_x = float(tok[1]) - current_y = float(tok[2]) + current_x = float(tok[0]) + current_y = float(tok[1]) return current_x, current_y def get_nanocube_z(self, probe: ProbeSentio) -> float: @@ -292,5 +292,5 @@ def get_nanocube_z(self, probe: ProbeSentio) -> float: # Parse response message tok = resp.message().split(",") - current_z = float(tok[1]) + current_z = float(tok[0]) return current_z \ No newline at end of file diff --git a/sentio_prober_control/UnitTest/TestProbeCommandGroup.py b/sentio_prober_control/UnitTest/TestProbeCommandGroup.py index 8106552..1325e6e 100644 --- a/sentio_prober_control/UnitTest/TestProbeCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestProbeCommandGroup.py @@ -1,9 +1,6 @@ import unittest from unittest.mock import MagicMock -from sentio_prober_control.Communication.CommunicatorBase import CommunicatorBase from sentio_prober_control.Sentio.Enumerations import ProbeSentio, ProbeXYReference, ProbeZReference -from sentio_prober_control.Sentio.Response import Response -from sentio_prober_control.Sentio.CommandGroups.ProbeCommandGroup import ProbeCommandGroup from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp from sentio_prober_control.Sentio.ProberSentio import SentioProber class TestProbeCommandGroup(unittest.TestCase): @@ -19,12 +16,11 @@ def test_async_step_probe_site(self): self.mock_comm.send.assert_called_with("start_step_positioner_site East,1") self.assertEqual(cmd_id, 123) - #----Command is different of document, waiting the fix by another PR--- - # def test_get_probe_site(self): - # self.mock_comm.read_line.return_value = "0,0,site1,1000,2000" - # result = self.test_prober.probe.get_probe_site(ProbeSentio.East, 0) - # self.mock_comm.send.assert_called_with("get_positioner_site East,0") - # self.assertEqual(result, (0, "site1", 1000.0, 2000.0)) + def test_get_probe_site(self): + self.mock_comm.read_line.return_value = "0,0,site1,1000,2000,Home" + result = self.test_prober.probe.get_probe_site(ProbeSentio.East, 0) + self.mock_comm.send.assert_called_with("get_positioner_site East,0") + self.assertEqual(result, ("site1", 1000.0, 2000.0, "Home")) def test_get_probe_xy(self): self.mock_comm.read_line.return_value = "0,0,1500,2500" diff --git a/sentio_prober_control/UnitTest/TestSiPHCommanfGroup.py b/sentio_prober_control/UnitTest/TestSiPHCommanfGroup.py new file mode 100644 index 0000000..6517dbe --- /dev/null +++ b/sentio_prober_control/UnitTest/TestSiPHCommanfGroup.py @@ -0,0 +1,122 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.Enumerations import ProbeSentio, UvwAxis, FiberType +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Sentio.Response import Response + +class TestSiPHCommandGroup(unittest.TestCase): + def setUp(self): + """Initialize the mock communicator and SiPHCommandGroup instance.""" + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + + # Ensure the mock provides `send` and `read_line` methods + self.test_prober = SentioProber(self.mock_comm) + + def test_fast_alignment(self): + """Test fast_alignment method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.fast_alignment() + self.mock_comm.send.assert_called_with("siph:fast_alignment") + + def test_get_cap_sensor(self): + """Test get_cap_sensor method.""" + self.mock_comm.read_line.return_value = "0,0,0.5,1.2" + cap1, cap2 = self.test_prober.siph.get_cap_sensor() + self.mock_comm.send.assert_called_with("siph:get_cap_sensor") + self.assertEqual(cap1, 0.5) + self.assertEqual(cap2, 1.2) + + def test_get_intensity(self): + """Test get_intensity method.""" + self.mock_comm.read_line.return_value = "0,0,1.5" + intensity = self.test_prober.siph.get_intensity(1) + self.mock_comm.send.assert_called_with("siph:get_intensity 1") + self.assertEqual(intensity, 1.5) + + def test_gradient_search(self): + """Test gradient_search method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.gradient_search() + self.mock_comm.send.assert_called_with("siph:gradient_search") + + def test_move_hover(self): + """Test move_hover method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.move_hover(ProbeSentio.East) + self.mock_comm.send.assert_called_with("siph:move_hover East") + + def test_coupling(self): + """Test coupling method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.coupling(ProbeSentio.East, UvwAxis.V) + self.mock_comm.send.assert_called_with("siph:coupling East,V") + + def test_get_alignment(self): + """Test get_alignment method.""" + self.mock_comm.read_line.return_value = "0,0,true,false,true,true" + coarse, fine, gradient, rotary = self.test_prober.siph.get_alignment(ProbeSentio.West, FiberType.Single) + self.mock_comm.send.assert_called_with("siph:get_alignment West,Single") + self.assertTrue(coarse) + self.assertFalse(fine) + self.assertTrue(gradient) + self.assertTrue(rotary) + + def test_set_origin(self): + """Test set_origin method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.set_origin(ProbeSentio.East) + self.mock_comm.send.assert_called_with("siph:set_origin East") + + def test_move_origin(self): + """Test move_origin method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.move_origin(ProbeSentio.West) + self.mock_comm.send.assert_called_with("siph:move_origin West") + + def test_move_position_uvw(self): + """Test move_position_uvw method.""" + self.mock_comm.read_line.return_value = "0,0,0.2" + position = self.test_prober.siph.move_position_uvw(ProbeSentio.East, UvwAxis.U, 0.1) + self.mock_comm.send.assert_called_with("siph:move_position_uvw East,U,0.1") + self.assertEqual(position, 0.2) + + def test_pivot_point(self): + """Test pivot_point method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.pivot_point(ProbeSentio.West) + self.mock_comm.send.assert_called_with("siph:pivot_point West") + + def test_move_nanocube_xy(self): + """Test move_nanocube_xy method.""" + self.mock_comm.read_line.return_value = "0,0,50.000,50.000" + x, y = self.test_prober.siph.move_nanocube_xy(ProbeSentio.East, 50, 50) + self.mock_comm.send.assert_called_with("siph:move_nanocube_xy East,50,50") + self.assertEqual(x, 50.000) + self.assertEqual(y, 50.000) + + def test_get_nanocube_xy(self): + """Test get_nanocube_xy method.""" + self.mock_comm.read_line.return_value = "0,0,50.000,50.000" + x, y = self.test_prober.siph.get_nanocube_xy(ProbeSentio.East) + self.mock_comm.send.assert_called_with("siph:get_nanocube_xy East") + self.assertEqual(x, 50.000) + self.assertEqual(y, 50.000) + + def test_get_nanocube_z(self): + """Test get_nanocube_z method.""" + self.mock_comm.read_line.return_value = "0,0,50.000" + z = self.test_prober.siph.get_nanocube_z(ProbeSentio.West) + self.mock_comm.send.assert_called_with("siph:get_nanocube_z West") + self.assertEqual(z, 50.000) + + def test_start_tracking(self): + """Test start_tracking method.""" + self.mock_comm.read_line.return_value = "0,5,OK" + command_id = self.test_prober.siph.start_tracking(30) + self.mock_comm.send.assert_called_with("siph:start_tracking 30") + self.assertEqual(command_id, 5) + + +if __name__ == "__main__": + unittest.main() From 1dadaa8443737e597651c90ab9fae26f2b472c9c Mon Sep 17 00:00:00 2001 From: Hoca Chen Date: Sat, 15 Mar 2025 11:24:37 +0800 Subject: [PATCH 5/5] add scope commands and Unit Test --- sentio_prober_control/Sentio/ProberSentio.py | 95 +++++++++++++++++++ .../UnitTest/TestProberSentio.py | 66 +++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 sentio_prober_control/UnitTest/TestProberSentio.py diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 7e3101d..4d25601 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -1050,3 +1050,98 @@ def wait_complete(self, id_or_resp: int | Response, timeout: int = 300) -> Respo self.comm.send(f"wait_complete {id_or_resp}, {timeout}") return Response.check_resp(self.comm.read_line()) + + def get_scope_home(self) -> tuple[float, float]: + """Gets the home position information for the scope stage. + + Returns: + A tuple containing the X and Y home positions in micrometers. + """ + self.comm.send("get_scope_home") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + home_x = float(tok[0]) + home_y = float(tok[1]) + return home_x, home_y + + def set_scope_home(self, home_x: float = None, home_y: float = None) -> None: + """Sets the home position for the scope stage. + + Args: + home_x: (Optional) The X-coordinate of the home position in micrometers. + home_y: (Optional) The Y-coordinate of the home position in micrometers. + + Returns: + A Response object indicating whether the command was successful. + """ + if home_x is not None and home_y is not None: + self.comm.send(f"set_scope_home {home_x},{home_y}") + else: + self.comm.send("set_scope_home") + + Response.check_resp(self.comm.read_line()) + + def step_scope_site(self, site_index: int or str) -> tuple[str, float, float]: + """Steps the scope to the indicated site and sets it as the current site. + + Args: + site_index: The scope site index (zero-based) or its ID. + + Returns: + A tuple containing: + - Site ID (string): The identifier of the current scope site. + - Offset X (float): The X offset relative to the scope home position. + - Offset Y (float): The Y offset relative to the scope home position. + """ + self.comm.send(f"step_scope_site {site_index}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_id = tok[0] + offset_x = float(tok[1]) + offset_y = float(tok[2]) + + return site_id, offset_x, offset_y + + def step_scope_site_first(self) -> tuple[str, float, float]: + """Steps the scope to the first site in the scope site list. + + Returns: + A tuple containing: + - Site ID (string): The identifier of the current scope site. + - Offset X (float): The X offset relative to the scope home position. + - Offset Y (float): The Y offset relative to the scope home position. + """ + self.comm.send("step_scope_site_first") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_id = tok[0] + offset_x = float(tok[1]) + offset_y = float(tok[2]) + + return site_id, offset_x, offset_y + + def step_scope_site_next(self) -> tuple[str, float, float]: + """Steps the scope to the next site and sets it as the current site. + + Returns: + A tuple containing: + - Site ID (string): The identifier of the current scope site. + - Offset X (float): The X offset relative to the scope home position. + - Offset Y (float): The Y offset relative to the scope home position. + """ + self.comm.send("step_scope_site_next") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_id = tok[0] + offset_x = float(tok[1]) + offset_y = float(tok[2]) + + return site_id, offset_x, offset_y \ No newline at end of file diff --git a/sentio_prober_control/UnitTest/TestProberSentio.py b/sentio_prober_control/UnitTest/TestProberSentio.py new file mode 100644 index 0000000..3a81988 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestProberSentio.py @@ -0,0 +1,66 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Sentio.Response import Response + +class TestScopeCommandGroup(unittest.TestCase): + def setUp(self): + """Initialize the mock communicator and ScopeCommandGroup instance.""" + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + + # Ensure the mock provides `send` and `read_line` methods + self.test_prober = SentioProber(self.mock_comm) + + def test_get_scope_home(self): + """Test get_scope_home method.""" + self.mock_comm.read_line.return_value = "0,0,250,250" + home_x, home_y = self.test_prober.get_scope_home() + self.mock_comm.send.assert_called_with("get_scope_home") + self.assertEqual(home_x, 250.0) + self.assertEqual(home_y, 250.0) + + def test_set_scope_home_with_values(self): + """Test set_scope_home method with specified X and Y values.""" + self.mock_comm.read_line.return_value = "0,0,OK" + response = self.test_prober.set_scope_home(300, 400) + self.mock_comm.send.assert_called_with("set_scope_home 300,400") + self.assertIsNone(response) # The method does not return a value + + def test_set_scope_home_without_values(self): + """Test set_scope_home method with no arguments (default to current position).""" + self.mock_comm.read_line.return_value = "0,0,OK" + response = self.test_prober.set_scope_home() + self.mock_comm.send.assert_called_with("set_scope_home") + self.assertIsNone(response) + + def test_step_scope_site(self): + """Test step_scope_site method.""" + self.mock_comm.read_line.return_value = "0,0,Pos1,1000,2000" + site_id, offset_x, offset_y = self.test_prober.step_scope_site(2) + self.mock_comm.send.assert_called_with("step_scope_site 2") + self.assertEqual(site_id, "Pos1") + self.assertEqual(offset_x, 1000.0) + self.assertEqual(offset_y, 2000.0) + + def test_step_scope_site_first(self): + """Test step_scope_site_first method.""" + self.mock_comm.read_line.return_value = "0,0,Pos1,0,0" + site_id, offset_x, offset_y = self.test_prober.step_scope_site_first() + self.mock_comm.send.assert_called_with("step_scope_site_first") + self.assertEqual(site_id, "Pos1") + self.assertEqual(offset_x, 0.0) + self.assertEqual(offset_y, 0.0) + + def test_step_scope_site_next(self): + """Test step_scope_site_next method.""" + self.mock_comm.read_line.return_value = "0,0,Pos2,1000,1000" + site_id, offset_x, offset_y = self.test_prober.step_scope_site_next() + self.mock_comm.send.assert_called_with("step_scope_site_next") + self.assertEqual(site_id, "Pos2") + self.assertEqual(offset_x, 1000.0) + self.assertEqual(offset_y, 1000.0) + + +if __name__ == "__main__": + unittest.main()