From c991fa9e8bc7369b266c63204fe9d2dab61be3f4 Mon Sep 17 00:00:00 2001 From: Cynthia Date: Mon, 24 Mar 2025 17:51:44 +0800 Subject: [PATCH 01/41] Added map commands & unit test 1. map:path series 2. map:poi series 3. map:subsite series 4. map:view series --- .../CommandGroups/WafermapPathCommandGroup.py | 33 ++++++- .../CommandGroups/WafermapPoiCommandGroup.py | 19 +++- .../WafermapSubsiteCommandGroup.py | 93 ++++++++++++++++-- .../CommandGroups/WafermapViewCommandGroup.py | 14 +++ .../UnitTest/TestWafermapPathCommandGroup.py | 50 ++++++++++ .../UnitTest/TestWafermapPoiCommandGroup.py | 61 ++++++++++++ .../TestWafermapSubsiteCommandGroup.py | 98 +++++++++++++++++++ .../UnitTest/TestWafermapViewCommandGroup.py | 20 ++++ 8 files changed, 374 insertions(+), 14 deletions(-) create mode 100644 sentio_prober_control/Sentio/CommandGroups/WafermapViewCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestWafermapViewCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py index d98ed3b..d8281a4 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py @@ -22,7 +22,6 @@ def create_from_bin(self, bin_val: int) -> None: self.comm.send("map:path:create_from_bins {0}".format(bin_val)) Response.check_resp(self.comm.read_line()) - def get_die(self, seq: int) -> Tuple[int, int]: """Get die column and row coordinates from a sequence number. @@ -39,7 +38,6 @@ def get_die(self, seq: int) -> Tuple[int, int]: tok = resp.message().split(",") return int(tok[0]), int(tok[1]) - def select_dies(self, selection: TestSelection) -> None: """Select dies for testing. @@ -51,7 +49,6 @@ def select_dies(self, selection: TestSelection) -> None: self.comm.send(f"map:path:select_dies {selection.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) - def set_routing(self, sp: RoutingStartPoint, pri: RoutingPriority) -> None: """Set up path finnding for stepping by specifying a start point position and a row or column priority for routing. @@ -64,3 +61,33 @@ def set_routing(self, sp: RoutingStartPoint, pri: RoutingPriority) -> None: """ self.comm.send(f"map:set_routing {sp.toSentioAbbr()}, {pri.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) + + def add_bins(self, selection: str) -> int: + """Adds dies with specific bin(s) to the stepping path. + + Wraps SENTIO's map:path:add_bins remote command. + + Args: + selection: e.g. "1", "1-5", "Pass", "Unbinned" + + Returns: + Path length after addition. + """ + self.comm.send(f"map:path:add_bins {selection}") + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) + + def remove_bins(self, selection: str) -> int: + """Removes dies with specific bin(s) from stepping path. + + Wraps SENTIO's map:path:remove_bins remote command. + + Args: + selection: e.g. "1", "1-5", "Pass", "Fail" + + Returns: + Remaining path length. + """ + self.comm.send(f"map:path:remove_bins {selection}") + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py index f09eaa7..7f88f9f 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py @@ -19,7 +19,6 @@ def add(self, x: float, y: float, desc: str) -> None: self.comm.send(f"map:poi:add {x}, {y}, {desc}") Response.check_resp(self.comm.read_line()) - def get_num(self) -> int: """Returns the number of POIs in the list. @@ -32,7 +31,6 @@ def get_num(self) -> int: resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) - def reset(self, stage: Stage, refXy: PoiReferenceXy) -> None: """Reset the list of POIs. @@ -47,7 +45,6 @@ def reset(self, stage: Stage, refXy: PoiReferenceXy) -> None: self.comm.send("map:poi:reset {0}, {1}".format(stage.toSentioAbbr(), refXy.toSentioAbbr())) Response.check_resp(self.comm.read_line()) - def step(self, target: str | int) -> None: """Step to a POI in the list. @@ -59,7 +56,6 @@ def step(self, target: str | int) -> None: self.comm.send(f"map:poi:step {target}") Response.check_resp(self.comm.read_line()) - def step_first(self) -> None: """Step to the first POI in the list. @@ -68,7 +64,6 @@ def step_first(self) -> None: self.comm.send("map:poi:step_first") Response.check_resp(self.comm.read_line()) - def step_next(self) -> None: """Step to the next POI in the list. @@ -76,3 +71,17 @@ def step_next(self) -> None: """ self.comm.send("map:poi:step_next") Response.check_resp(self.comm.read_line()) + + def remove(self, idx: int | None = None) -> None: + """Remove POI(s) from wafermap. + + Wraps SENTIO's map:poi:remove remote command. + + Args: + idx: POI index to remove. If None, remove all. + """ + if idx is None: + self.comm.send("map:poi:remove") + else: + self.comm.send(f"map:poi:remove {idx}") + Response.check_resp(self.comm.read_line()) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py index 02854c2..b315a77 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py @@ -18,7 +18,6 @@ def __init__(self, comm, wafermap_command_group) -> None: super().__init__(comm) self._parent_command_group = wafermap_command_group - def add(self, id: str, x: float, y: float, orient: AxisOrient = AxisOrient.UpRight) -> None: """Add a single subsite to the wafermap. @@ -37,7 +36,6 @@ def add(self, id: str, x: float, y: float, orient: AxisOrient = AxisOrient.UpRig self.comm.send("map:subsite:add {}, {}, {}, {}".format(id, x, y, orient.toSentioAbbr())) Response.check_resp(self.comm.read_line()) - def bin_step_next(self, bin: int) -> Tuple[int, int, int]: """Step to the next active subsite and assign bin code to current subsite. @@ -57,7 +55,6 @@ def bin_step_next(self, bin: int) -> Tuple[int, int, int]: tok = resp.message().split(",") return int(tok[0]), int(tok[1]), int(tok[2]) - def get(self, idx: int, orient: AxisOrient | None = None) -> Tuple[str, float, float]: """Returns the subsite definition for a subsite with a given index. @@ -85,7 +82,6 @@ def get(self, idx: int, orient: AxisOrient | None = None) -> Tuple[str, float, f tok = resp.message().split(",") return str(tok[0]), float(tok[1]), float(tok[2]) - def get_num(self) -> int: """Retrieve the number of subsites per die defined in the wafermap. @@ -98,7 +94,6 @@ def get_num(self) -> int: resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) - def reset(self) -> None: """Reset Sentios subsite definitions. @@ -107,7 +102,6 @@ def reset(self) -> None: self.comm.send("map:subsite:reset") Response.check_resp(self.comm.read_line()) - def step_next(self) -> Tuple[int, int, int]: """Step to the next active subsite. @@ -123,3 +117,90 @@ def step_next(self) -> Tuple[int, int, int]: tok = resp.message().split(",") return int(tok[0]), int(tok[1]), int(tok[2]) + + def export(self, file_path: str) -> None: + """Export subsite definitions to file. + + Wraps the "map:subsite:export" remote command. + + Args: + file_path: Full or relative file path to export (CSV, XLS, XLSX) + """ + self.comm.send(f"map:subsite:export {file_path}") + Response.check_resp(self.comm.read_line()) + + def get_state(self, subsite: int | str, col: int | None = None, row: int | None = None) -> int: + """Check if a subsite is active (globally or locally). + + Wraps the "map:subsite:get_state" remote command. + + Args: + subsite: Subsite index or ID. + col: Optional column for local state check. + row: Optional row for local state check. + + Returns: + 1 if active, 0 if inactive. + """ + if col is not None and row is not None: + self.comm.send(f"map:subsite:get_state {subsite}, {col}, {row}") + else: + self.comm.send(f"map:subsite:get_state {subsite}") + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) + + def import_from_file(self, file_path: str) -> None: + """Import subsite definitions from file. + + Wraps the "map:subsite:import" remote command. + + Args: + file_path: Path to CSV/XLS/XLSX file. + """ + self.comm.send(f"map:subsite:import {file_path}") + Response.check_resp(self.comm.read_line()) + + def remove(self, subsite: int | str) -> None: + """Remove a subsite from the table. + + Wraps the "map:subsite:remove" remote command. + + Args: + subsite: Index or ID of the subsite to remove. + """ + self.comm.send(f"map:subsite:remove {subsite}") + Response.check_resp(self.comm.read_line()) + + def set_state(self, subsite: int | str, state: int, col: int | None = None, row: int | None = None) -> None: + """Set a subsite active or inactive. + + Wraps the "map:subsite:set_state" remote command. + + Args: + subsite: Subsite ID or index. + state: 1 = active, 0 = inactive. + col: Optional column index for local state. + row: Optional row index for local state. + """ + if col is not None and row is not None: + self.comm.send(f"map:subsite:set_state {subsite}, {state}, {col}, {row}") + else: + self.comm.send(f"map:subsite:set_state {subsite}, {state}") + Response.check_resp(self.comm.read_line()) + + def step_previous(self) -> Tuple[int, int, int]: + """Step to the previous active subsite. + + Wraps the "map:subsite:step_previous" remote command. + + Returns: + A tuple of (col, row, subsite) after step. + """ + self.comm.send("map:subsite:step_previous") + resp = Response.check_resp(self.comm.read_line()) + self._parent_command_group.__end_of_route = (resp.status() & StatusBits.EndOfRoute) == StatusBits.EndOfRoute + + tok = resp.message().split(",") + return int(tok[0]), int(tok[1]), int(tok[2]) + + diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapViewCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapViewCommandGroup.py new file mode 100644 index 0000000..4a661f5 --- /dev/null +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapViewCommandGroup.py @@ -0,0 +1,14 @@ +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase + + +class WafermapViewCommandGroup(CommandGroupBase): + """This command group provides functions related to wafermap view settings.""" + + def show_current_die(self) -> None: + """Show the current die in the wafermap view. + + Wraps SENTIO's "map:view:show_current_die" remote command. + """ + self.comm.send("map:view:show_current_die") + Response.check_resp(self.comm.read_line()) diff --git a/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py new file mode 100644 index 0000000..7531b1a --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py @@ -0,0 +1,50 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.Enumerations import RoutingPriority, RoutingStartPoint, TestSelection, RoutingPriority + + +class TestWafermapPathCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.prober = SentioProber(self.mock_comm) + + def test_create_from_bin(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.path.create_from_bin(3) + self.mock_comm.send.assert_called_with("map:path:create_from_bins 3") + + def test_get_die(self): + self.mock_comm.read_line.return_value = "0,0,10,20" + result = self.prober.map.path.get_die(5) + self.mock_comm.send.assert_called_with("map:path:get_die 5") + self.assertEqual(result, (10, 20)) + + def test_select_dies(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.path.select_dies(TestSelection.All) + self.mock_comm.send.assert_called_with("map:path:select_dies a") + + def test_set_routing(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.path.set_routing(RoutingStartPoint.UpperLeft, RoutingPriority.RowUniDir) + self.mock_comm.send.assert_called_with("map:set_routing ul, r") + + def test_add_bins(self): + self.mock_comm.read_line.return_value = "0,0,12" + result = self.prober.map.path.add_bins("1-5") + self.mock_comm.send.assert_called_with("map:path:add_bins 1-5") + self.assertEqual(result, 12) + + def test_remove_bins(self): + self.mock_comm.read_line.return_value = "0,0,6" + result = self.prober.map.path.remove_bins("Fail") + self.mock_comm.send.assert_called_with("map:path:remove_bins Fail") + self.assertEqual(result, 6) + + +if __name__ == "__main__": + unittest.main() + + diff --git a/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py new file mode 100644 index 0000000..e8ca7f6 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py @@ -0,0 +1,61 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.Enumerations import PoiReferenceXy, Stage + + +class TestWafermapPoiCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.prober = SentioProber(self.mock_comm) + + def test_add(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.add(100.0, 200.0, "MyPOI") + self.mock_comm.send.assert_called_with("map:poi:add 100.0, 200.0, MyPOI") + + def test_get_num(self): + self.mock_comm.read_line.return_value = "0,0,5" + result = self.prober.map.poi.get_num() + self.mock_comm.send.assert_called_with("map:poi:get_num") + self.assertEqual(result, 5) + + def test_reset(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.reset(Stage.Chuck, PoiReferenceXy.DieCenter) + self.mock_comm.send.assert_called_with("map:poi:reset chuck, DieCenter") + + def test_step_by_index(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.step(2) + self.mock_comm.send.assert_called_with("map:poi:step 2") + + def test_step_by_id(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.step("POI_ID") + self.mock_comm.send.assert_called_with("map:poi:step POI_ID") + + def test_step_first(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.step_first() + self.mock_comm.send.assert_called_with("map:poi:step_first") + + def test_step_next(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.step_next() + self.mock_comm.send.assert_called_with("map:poi:step_next") + + def test_remove_all(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.remove() + self.mock_comm.send.assert_called_with("map:poi:remove") + + def test_remove_by_index(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.poi.remove(3) + self.mock_comm.send.assert_called_with("map:poi:remove 3") + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py new file mode 100644 index 0000000..dbf04e8 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py @@ -0,0 +1,98 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.Enumerations import AxisOrient, StatusBits +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp + + +class TestWafermapSubsiteGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.prober = SentioProber(self.mock_comm) + + def test_add(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.subsites.add("A", 100.0, 200.0, AxisOrient.UpLeft) + self.mock_comm.send.assert_called_with("map:subsite:add A, 100.0, 200.0, UL") + + def test_bin_step_next(self): + self.mock_comm.read_line.return_value = "0,0,5,6,1" + result = self.prober.map.subsites.bin_step_next(2) + self.mock_comm.send.assert_called_with("map:subsite:bin_step_next 2") + self.assertEqual(result, (5, 6, 1)) + + def test_get_with_orientation(self): + self.mock_comm.read_line.return_value = "0,0,Site1,100.0,200.0" + result = self.prober.map.subsites.get(0, AxisOrient.DownRight) + self.mock_comm.send.assert_called_with("map:subsite:get 0, DR") + self.assertEqual(result, ("Site1", 100.0, 200.0)) + + def test_get_with_default_orientation(self): + self.mock_comm.read_line.return_value = "0,0,SiteX,123.4,567.8" + result = self.prober.map.subsites.get(1) + self.mock_comm.send.assert_called_with("map:subsite:get 1, MAP") + self.assertEqual(result, ("SiteX", 123.4, 567.8)) + + def test_get_num(self): + self.mock_comm.read_line.return_value = "0,0,4" + result = self.prober.map.subsites.get_num() + self.mock_comm.send.assert_called_with("map:subsite:get_num") + self.assertEqual(result, 4) + + def test_reset(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.subsites.reset() + self.mock_comm.send.assert_called_with("map:subsite:reset") + + def test_step_next(self): + self.mock_comm.read_line.return_value = "0,0,8,9,2" + result = self.prober.map.subsites.step_next() + self.mock_comm.send.assert_called_with("map:subsite:step_next") + self.assertEqual(result, (8, 9, 2)) + + def test_export(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.subsites.export("subsite_export.csv") + self.mock_comm.send.assert_called_with("map:subsite:export subsite_export.csv") + + def test_get_state_global(self): + self.mock_comm.read_line.return_value = "0,0,1" + result = self.prober.map.subsites.get_state("Site1") + self.mock_comm.send.assert_called_with("map:subsite:get_state Site1") + self.assertEqual(result, 1) + + def test_get_state_local(self): + self.mock_comm.read_line.return_value = "0,0,0" + result = self.prober.map.subsites.get_state(1, 5, 6) + self.mock_comm.send.assert_called_with("map:subsite:get_state 1, 5, 6") + self.assertEqual(result, 0) + + def test_import_from_file(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.subsites.import_from_file("subsite_import.xlsx") + self.mock_comm.send.assert_called_with("map:subsite:import subsite_import.xlsx") + + def test_remove(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.subsites.remove("SiteToDelete") + self.mock_comm.send.assert_called_with("map:subsite:remove SiteToDelete") + + def test_set_state_global(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.subsites.set_state("Sub1", 1) + self.mock_comm.send.assert_called_with("map:subsite:set_state Sub1, 1") + + def test_set_state_local(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.subsites.set_state("Sub1", 0, 2, 3) + self.mock_comm.send.assert_called_with("map:subsite:set_state Sub1, 0, 2, 3") + + def test_step_previous(self): + self.mock_comm.read_line.return_value = "0,0,2,3,1" + result = self.prober.map.subsites.step_previous() + self.mock_comm.send.assert_called_with("map:subsite:step_previous") + self.assertEqual(result, (2, 3, 1)) + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestWafermapViewCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapViewCommandGroup.py new file mode 100644 index 0000000..f56a8e6 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapViewCommandGroup.py @@ -0,0 +1,20 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.ProberSentio import SentioProber + + +class TestWafermapViewCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.prober = SentioProber(self.mock_comm) + + def test_show_current_die(self): + """Test map:view:show_current_die command""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.view.show_current_die() + self.mock_comm.send.assert_called_with("map:view:show_current_die") + + +if __name__ == "__main__": + unittest.main() From 83d33da8691e55a944cc98e01427b9f1907ded00 Mon Sep 17 00:00:00 2001 From: Cynthia Date: Mon, 24 Mar 2025 17:54:18 +0800 Subject: [PATCH 02/41] Added map commands & unit test 1. map:compensation series 2. map:die series --- .../WafermapCompensationCommandGroup.py | 33 ++++++++-- .../CommandGroups/WafermapDieCommandGroup.py | 58 +++++++++++++++--- .../TestWafermapCompensationCommandGroup.py | 40 +++++++++++++ .../UnitTest/TestWafermapDieCommandGroup.py | 60 +++++++++++++++++++ 4 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestWafermapDieCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py index a16debb..6e43085 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py @@ -1,6 +1,5 @@ from deprecated import deprecated - -from sentio_prober_control.Sentio.Enumerations import ExecuteAction +from sentio_prober_control.Sentio.Enumerations import ExecuteAction, XyCompensationType, ZCompensationType from sentio_prober_control.Sentio.ProberBase import ProberException from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase @@ -8,14 +7,40 @@ @deprecated("Use VisionCompensationGroup instead") class WafermapCompensationCommandGroup(CommandGroupBase): + """This command group bundles functions for setting up and using XY/Z compensation on the wafermap.""" @deprecated("Use vis.compensation.start_execute(CompensationType.Topography, CompensationMode.Vertical) instead") - def topography(self, execute: ExecuteAction): + def topography(self, execute: ExecuteAction) -> int: + """Execute topography compensation. + + Args: + execute: The action to execute. + + Returns: + The command ID of the asynchronous operation. + """ self.comm.send(f"map:compensation:topography {execute.toSentioAbbr()}") resp = Response.check_resp(self.comm.read_line()) - # i.e. Stepping while at the end of the route if not resp.ok(): raise ProberException(resp.message()) return resp.cmd_id() + + def set_xy(self, comp_type: XyCompensationType) -> None: + """Enable the XY Stepping Compensation. + + Args: + comp_type: The type of XY Stepping Compensation. + """ + self.comm.send(f"map:compensation:set_xy {comp_type.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_z(self, comp_type: ZCompensationType) -> None: + """Enable the Z Stepping Compensation. + + Args: + comp_type: The type of Z Stepping Compensation. + """ + self.comm.send(f"map:compensation:set_z {comp_type.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapDieCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapDieCommandGroup.py index 09f79d0..79be93b 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapDieCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapDieCommandGroup.py @@ -1,3 +1,4 @@ +from typing import Tuple from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase @@ -6,8 +7,7 @@ class WafermapDieCommandGroup(CommandGroupBase): """This Command group bundles commands for setting up dies on a wafermap.""" def add(self, x: int, y: int) -> None: - """Add a die to the wafermap. If the die is already part of the map - nothing happens. + """Add a die to the wafermap. If the die is already part of the map, nothing happens. Args: x: The column of the die. @@ -16,11 +16,10 @@ def add(self, x: int, y: int) -> None: self.comm.send(f"map:die:add {x}, {y}") Response.check_resp(self.comm.read_line()) - def remove(self, x: int, y: int) -> None: """Remove a die from the wafermap. - This will mark the die as nonexistant and make it unavailable for stepping. + This will mark the die as nonexistent and make it unavailable for stepping. Removed dies are treated as if they were physically not present on the wafer. Wraps SENTIO's map:die:remove remote command. @@ -32,7 +31,6 @@ def remove(self, x: int, y: int) -> None: self.comm.send(f"map:die:remove {x}, {y}") Response.check_resp(self.comm.read_line()) - def select(self, x: int, y: int) -> None: """Selects a die for testing by adding it to the test path. @@ -40,10 +38,9 @@ def select(self, x: int, y: int) -> None: x: The column of the die. y: The row of the die. """ - self.comm.send(f"map:die:add {x}, {y}") + self.comm.send(f"map:die:select {x}, {y}") Response.check_resp(self.comm.read_line()) - def unselect(self, x: int, y: int) -> None: """Unselects a die from testing by removing it from the test path. @@ -53,3 +50,50 @@ def unselect(self, x: int, y: int) -> None: """ self.comm.send(f"map:die:unselect {x}, {y}") Response.check_resp(self.comm.read_line()) + + def get_current_index(self) -> Tuple[int, int, int]: + """Retrieves information about the current die. + + Returns: + A tuple containing: + - List Index (int): The index in the routing list. + - Column Index (int): The column index relative to the grid origin. + - Row Index (int): The row index relative to the grid origin. + """ + self.comm.send("map:die:get_current_index") + resp = Response.check_resp(self.comm.read_line()) + + values = resp.message().split(",") + if len(values) < 3: + raise ValueError(f"Invalid response: {resp.message()}") + + return int(values[0]), int(values[1]), int(values[2]) + + def get_status(self, col_index: int, row_index: int) -> int: + """Retrieves information about presence and selection of a die. + + Args: + col_index: Column of the die relative to the grid origin. + row_index: Row number of the die relative to the grid origin. + + Returns: + Status (int): + - `1`: Die is selected. + - `2`: Die is not selected. + - `3`: Die is not present. + """ + self.comm.send(f"map:die:get_status {col_index}, {row_index}") + resp = Response.check_resp(self.comm.read_line()) + + return int(resp.message()) + + def get_current_subsite(self) -> int: + """Retrieves information about the currently active subsite. + + Returns: + Subsite Index (int): The index of the currently active subsite. + """ + self.comm.send("map:die:get_current_subsite") + resp = Response.check_resp(self.comm.read_line()) + + return int(resp.message()) diff --git a/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py new file mode 100644 index 0000000..7c57dea --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py @@ -0,0 +1,40 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorBase import CommunicatorBase +from sentio_prober_control.Sentio.Enumerations import ExecuteAction, XyCompensationType, ZCompensationType +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.CommandGroups.WafermapCompensationCommandGroup import WafermapCompensationCommandGroup +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.ProberSentio import SentioProber + + +class TestWafermapCompensationCommandGroup(unittest.TestCase): + def setUp(self): + # Mock the TCP/IP communicator + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + + # Create a test prober instance with the mocked communicator + self.test_prober = SentioProber(self.mock_comm) + + def test_topography(self): + """Test executing topography compensation""" + self.mock_comm.read_line.return_value = "0,100,OK" + cmd_id = self.test_prober.map.compensation.topography(ExecuteAction.Execute) + self.mock_comm.send.assert_called_with("map:compensation:topography execute") + self.assertEqual(cmd_id, 100) + + def test_set_xy_compensation(self): + """Test enabling XY compensation""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.compensation.set_xy(XyCompensationType.OnTheFly) + self.mock_comm.send.assert_called_with("map:compensation:set_xy OnTheFly") + + def test_set_z_compensation(self): + """Test enabling Z compensation""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.compensation.set_z(ZCompensationType.Topography) + self.mock_comm.send.assert_called_with("map:compensation:set_z Topography") + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestWafermapDieCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapDieCommandGroup.py new file mode 100644 index 0000000..74467e5 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapDieCommandGroup.py @@ -0,0 +1,60 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.ProberSentio import SentioProber + + +class TestWafermapDieCommandGroup(unittest.TestCase): + def setUp(self): + """Mock the communicator and initialize the test prober.""" + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.test_prober = SentioProber(self.mock_comm) + + def test_add_die(self): + """Test adding a die""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.die.add(2, 3) + self.mock_comm.send.assert_called_with("map:die:add 2, 3") + + def test_remove_die(self): + """Test removing a die""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.die.remove(2, 3) + self.mock_comm.send.assert_called_with("map:die:remove 2, 3") + + def test_select_die(self): + """Test selecting a die""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.die.select(2, 3) + self.mock_comm.send.assert_called_with("map:die:select 2, 3") + + def test_unselect_die(self): + """Test unselecting a die""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.die.unselect(2, 3) + self.mock_comm.send.assert_called_with("map:die:unselect 2, 3") + + def test_get_status(self): + """Test retrieving die status""" + self.mock_comm.read_line.return_value = "0,0,1" + result = self.test_prober.map.die.get_status(2, 3) + self.mock_comm.send.assert_called_with("map:die:get_status 2, 3") + self.assertEqual(result, 1) # 1 = Die is selected + + def test_get_current_index(self): + """Test retrieving current die index""" + self.mock_comm.read_line.return_value = "0,0,5,6,7" + result = self.test_prober.map.die.get_current_index() + self.mock_comm.send.assert_called_with("map:die:get_current_index") + self.assertEqual(result, (5, 6, 7)) + + def test_get_current_subsite(self): + """Test retrieving current active subsite""" + self.mock_comm.read_line.return_value = "0,0,2" + result = self.test_prober.map.die.get_current_subsite() + self.mock_comm.send.assert_called_with("map:die:get_current_subsite") + self.assertEqual(result, 2) + + +if __name__ == "__main__": + unittest.main() From 96afe414ce0447739a9b4551cfc4052b65e3a338 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 27 Mar 2025 15:02:42 +0800 Subject: [PATCH 03/41] Modify the rule of new API(LoaderCommandGroup&StatusCommandGroup) --- .../CommandGroups/LoaderCommandGroup.py | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py index f7383bb..2f16757 100644 --- a/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py @@ -60,8 +60,7 @@ def load_wafer(self, src_station: LoaderStation, src_slot: int, angle: int | Non else: self.comm.send(f"loader:load_wafer {src_station.toSentioAbbr()}, {src_slot}, {angle}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def prealign(self, marker: OrientationMarker, angle: int) -> None: """Prealign a wafer. @@ -74,8 +73,7 @@ def prealign(self, marker: OrientationMarker, angle: int) -> None: """ self.comm.send(f"loader:prealign {marker.toSentioAbbr()}, {angle}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def query_wafer_status(self, station : LoaderStation, slot : int) -> Tuple[LoaderStation, int, int, int, float] | None: """Query the status of a wafer in a loader station. @@ -143,8 +141,7 @@ def set_wafer_status(self, station: LoaderStation, slot : int, what : WaferStatu value (float): The value to set. """ self.comm.send(f"loader:set_wafer_status {station.toSentioAbbr()},{slot},{what.toSentioAbbr()},{value}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def start_prepare_station(self, station: LoaderStation, angle: float | None = None) -> None: """Prepare a loader station for wafer processing. @@ -164,16 +161,13 @@ def start_prepare_station(self, station: LoaderStation, angle: float | None = No else: self.comm.send(f"loader:start_prepare_station {station.toSentioAbbr()}, {angle}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) - @deprecated("duplicate functionality; Use SentioProber.move_chuck_work_area!") def switch_work_area(self, area: str): self.comm.send("move_chuck_work_area {0}".format(area)) resp = Response.check_resp(self.comm.read_line()) return resp.message() - def transfer_wafer( self, src_station: LoaderStation, @@ -192,9 +186,7 @@ def transfer_wafer( self.comm.send( f"loader:transfer_wafer {src_station.toSentioAbbr()}, {src_slot}, {dst_station.toSentioAbbr()}, {dst_slot}" ) - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - + Response.check_resp(self.comm.read_line()) def unload_wafer(self) -> None: """Unload the current wafer from the chuck. @@ -203,8 +195,7 @@ def unload_wafer(self) -> None: """ self.comm.send("loader:unload_wafer") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def has_cassette(self,station : LoaderStation) -> None: @@ -216,8 +207,7 @@ def has_cassette(self,station : LoaderStation) -> None: """ self.comm.send(f"loader:has_cassette {station.toSentioAbbr()}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def set_wafer_id(self,station : LoaderStation, slot : int, waferid : str) -> None: @@ -230,8 +220,7 @@ def set_wafer_id(self,station : LoaderStation, slot : int, waferid : str) -> Non """ self.comm.send(f"loader:set_wafer_id {station.toSentioAbbr}, {slot}, {waferid}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def query_wafer_id(self,station : LoaderStation, slot : int) -> None: @@ -243,8 +232,7 @@ def query_wafer_id(self,station : LoaderStation, slot : int) -> None: """ self.comm.send(f"loader:query_wafer_id {station.toSentioAbbr}, {slot}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def read_wafer_id(self,angle : str, side : str) -> None: @@ -256,8 +244,7 @@ def read_wafer_id(self,angle : str, side : str) -> None: """ self.comm.send(f"loader:read_wafer_id {angle}, {side}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def start_prepare_wafer(self,station : LoaderStation, slot : int, angle : int, readid : int, unloadstation : LoaderStation, unloadslot : int) -> None: @@ -266,8 +253,7 @@ def start_prepare_wafer(self,station : LoaderStation, slot : int, angle : int, r """ self.comm.send(f"loader:start_prepare_wafer {station.toSentioAbbr}, {slot}, {angle}, {readid}, {unloadstation}, {unloadslot}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def swap_wafer(self) ->None: @@ -278,8 +264,7 @@ def swap_wafer(self) ->None: """ self.comm.send(f"loader:swap_wafer") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def query_station_status(self, station : LoaderStation) ->None: @@ -291,8 +276,7 @@ def query_station_status(self, station : LoaderStation) ->None: """ self.comm.send(f"loader:query_station_status {station.toSentioAbbr}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + Response.check_resp(self.comm.read_line()) def start_read_wafer_id(self, angle : str, side : str) ->None: @@ -301,5 +285,4 @@ def start_read_wafer_id(self, angle : str, side : str) ->None: """ self.comm.send(f"loader:start_read_wafer_id {angle}, {side}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() \ No newline at end of file + Response.check_resp(self.comm.read_line()) From c10e0be61dd884f2e8d632b7bae16a8ecb04d6ef Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Fri, 28 Mar 2025 14:12:36 +0800 Subject: [PATCH 04/41] Modify TestLoaderCommandGroup.py --- .../UnitTest/TestLoaderCommandGroup.py | 122 +++++++----------- 1 file changed, 48 insertions(+), 74 deletions(-) diff --git a/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py index 75beccf..a71a7f0 100644 --- a/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py @@ -1,7 +1,19 @@ import unittest from unittest.mock import MagicMock, patch + from sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup import LoaderCommandGroup -from sentio_prober_control.Sentio.Enumerations import LoaderStation, OrientationMarker, WaferStatusItem + +# 🧰 小工具:建立有 toSentioAbbr() 的 mock enum +def fake_enum(value: str): + mock_enum = MagicMock() + mock_enum.toSentioAbbr.return_value = value + return mock_enum + + +def mock_response(mock_check, message): + mock_resp = MagicMock() + mock_resp.message.return_value = message + mock_check.return_value = mock_resp class TestLoaderCommandGroup(unittest.TestCase): @@ -13,108 +25,70 @@ def setUp(self): @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_has_station(self, mock_check): - mock_check.return_value.message.return_value = "1" - result = self.loader.has_station(LoaderStation.Cassette1) + mock_response(mock_check, "1") + station = fake_enum("cas1") + result = self.loader.has_station(station) self.assertTrue(result) self.mock_comm.send.assert_called_with("loader:has_station cas1") @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_load_wafer(self, mock_check): - mock_check.return_value.message.return_value = "OK" - msg = self.loader.load_wafer(LoaderStation.Cassette1, 1, 180) - self.assertEqual(msg, "OK") + mock_response(mock_check, "OK") + station = fake_enum("cas1") + self.loader.load_wafer(station, 1, 180) self.mock_comm.send.assert_called_with("loader:load_wafer cas1, 1, 180") @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_prealign(self, mock_check): - mock_check.return_value.message.return_value = "Aligned" - msg = self.loader.prealign(OrientationMarker.Notch, 90) - self.assertEqual(msg, "Aligned") + mock_response(mock_check, "OK") + marker = fake_enum("Notch") + self.loader.prealign(marker, 90) self.mock_comm.send.assert_called_with("loader:prealign Notch, 90") @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_query_wafer_status(self, mock_check): - mock_check.return_value.message.return_value = "Cassette1,1,200,90,50.0" - result = self.loader.query_wafer_status(LoaderStation.Cassette1, 1) - self.assertEqual(result, (LoaderStation.Cassette1, 1, 200, 90, 50.0)) - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_scan_station(self, mock_check): - mock_check.return_value.message.return_value = "11001" - msg = self.loader.scan_station(LoaderStation.Cassette2) - self.assertEqual(msg, "11001") + mock_response(mock_check, "Cassette1,1,200,90,50.0") + station = fake_enum("Cassette1") + result = self.loader.query_wafer_status(station, 1) + self.assertEqual(result, ("Cassette1", 1, 200, 90, 50.0)) @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_set_wafer_status(self, mock_check): - mock_check.return_value.message.return_value = "OK" - msg = self.loader.set_wafer_status(LoaderStation.Cassette1, 2, WaferStatusItem.Orientation, 180) - self.assertEqual(msg, "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_start_prepare_station(self, mock_check): - mock_check.return_value.message.return_value = "OK" - msg = self.loader.start_prepare_station(LoaderStation.Cassette2, 0) - self.assertEqual(msg, "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_transfer_wafer(self, mock_check): - mock_check.return_value.message.return_value = "OK" - msg = self.loader.transfer_wafer(LoaderStation.Cassette1, 1, LoaderStation.Chuck, 1) - self.assertEqual(msg, "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_unload_wafer(self, mock_check): - mock_check.return_value.message.return_value = "OK" - msg = self.loader.unload_wafer() - self.assertEqual(msg, "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_has_cassette(self, mock_check): - mock_check.return_value.message.return_value = "1" - msg = self.loader.has_cassette(LoaderStation.Cassette2) - self.assertEqual(msg, "1") + mock_response(mock_check, "OK") + station = fake_enum("cas1") + status_item = fake_enum("Orientation") + self.loader.set_wafer_status(station, 2, status_item, 180) + self.mock_comm.send.assert_called_with("loader:set_wafer_status cas1,2,Orientation,180") @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_set_wafer_id(self, mock_check): - mock_check.return_value.message.return_value = "wafer_123" - msg = self.loader.set_wafer_id(LoaderStation.Cassette1, 1, "wafer_123") - self.assertEqual(msg, "wafer_123") + mock_response(mock_check, "OK") + station = fake_enum("cas1") + self.loader.set_wafer_id(station, 1, "wafer_123") + self.mock_comm.send.assert_called_with("loader:set_wafer_id cas1, 1, wafer_123") @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_query_wafer_id(self, mock_check): - mock_check.return_value.message.return_value = "wafer_123" - msg = self.loader.query_wafer_id(LoaderStation.Cassette1, 1) - self.assertEqual(msg, "wafer_123") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_read_wafer_id(self, mock_check): - mock_check.return_value.message.return_value = "wafer_XYZ" - msg = self.loader.read_wafer_id("0", "T") - self.assertEqual(msg, "wafer_XYZ") + mock_response(mock_check, "wafer_123") + station = fake_enum("cas1") + self.loader.query_wafer_id(station, 1) + self.mock_comm.send.assert_called_with("loader:query_wafer_id cas1, 1") @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_start_prepare_wafer(self, mock_check): - mock_check.return_value.message.return_value = "OK" - msg = self.loader.start_prepare_wafer(LoaderStation.Cassette1, 1, 0, 1, LoaderStation.Cassette2, 2) - self.assertEqual(msg, "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_swap_wafer(self, mock_check): - mock_check.return_value.message.return_value = "OK" - msg = self.loader.swap_wafer() - self.assertEqual(msg, "OK") + mock_response(mock_check, "OK") + s1 = fake_enum("cas1") + s2 = fake_enum("cas2") + self.loader.start_prepare_wafer(s1, 1, 0, 1, s2, 2) + self.mock_comm.send.assert_called_with("loader:start_prepare_wafer cas1, 1, 0, 1, cas2, 2") @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") def test_query_station_status(self, mock_check): - mock_check.return_value.message.return_value = "000111" - msg = self.loader.query_station_status(LoaderStation.Cassette1) - self.assertEqual(msg, "000111") + mock_response(mock_check, "000111") + station = fake_enum("cas1") + self.loader.query_station_status(station) + self.mock_comm.send.assert_called_with("loader:query_station_status cas1") - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_start_read_wafer_id(self, mock_check): - mock_check.return_value.message.return_value = "wafer_abc" - msg = self.loader.start_read_wafer_id("0", "T") - self.assertEqual(msg, "wafer_abc") if __name__ == "__main__": unittest.main() From 8e8794c0bdc34719793de4b20d475034629516b2 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Fri, 28 Mar 2025 15:10:22 +0800 Subject: [PATCH 05/41] Modify statuscommandgroup.py rule,UnitTest and add enumeration --- .../CommandGroups/StatusCommandGroup.py | 44 +++-- sentio_prober_control/Sentio/Enumerations.py | 37 ++++ .../UnitTest/TestLoaderCommandGroup.py | 169 ++++++++--------- .../UnitTest/TestStatusCommandGroup.py | 178 +++++++++--------- 4 files changed, 235 insertions(+), 193 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 6d4f701..9aefd49 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -1,6 +1,10 @@ -from typing import Tuple - -from sentio_prober_control.Sentio.Enumerations import ThermoChuckState +from typing import Tuple, Union +from sentio_prober_control.Sentio.Enumerations import ( + ThermoChuckState, + ChuckThermoEnergyMode, + ChuckThermoHoldMode, + HighPurgeState +) from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ( ModuleCommandGroupBase, @@ -123,16 +127,16 @@ def set_chuck_temp(self, temp: float, lift_chuck : bool = False) -> None: self.comm.send(f"status:set_chuck_temp {temp:.2f}, {lift_chuck}") Response.check_resp(self.comm.read_line()) - def get_chuck_thermo_energy_mode(self) -> str: + def get_chuck_thermo_energy_mode(self) -> ChuckThermoEnergyMode: """Get the current chuck thermo energy mode. Returns: The current energy mode as a string. Possible values: Fast, Optimal, HighPower, Customized. """ self.comm.send("status:get_chuck_thermo_energy_mode") resp = Response.check_resp(self.comm.read_line()) - return resp.message() + return ChuckThermoEnergyMode[resp.message()] - def get_chuck_thermo_hold_mode(self) -> str: + def get_chuck_thermo_hold_mode(self) -> ChuckThermoHoldMode: """Get thermo chuck hold mode. Returns: @@ -140,9 +144,9 @@ def get_chuck_thermo_hold_mode(self) -> str: """ self.comm.send("status:get_chuck_thermo_hold_mode") resp = Response.check_resp(self.comm.read_line()) - return resp.message() + return ChuckThermoHoldMode[resp.message()] - def get_high_purge_state(self) -> str: + def get_high_purge_state(self) -> HighPurgeState: """Get thermo chuck high purge state. Returns: @@ -150,9 +154,9 @@ def get_high_purge_state(self) -> str: """ self.comm.send("status:get_high_purge_state") resp = Response.check_resp(self.comm.read_line()) - return resp.message() + return HighPurgeState[resp.message()] - def set_chuck_thermo_energy_mode(self, mode: str) -> Response: + def set_chuck_thermo_energy_mode(self, mode: Union[str, ChuckThermoEnergyMode]) -> Response: """Set chuck thermo energy mode. Args: @@ -164,10 +168,12 @@ def set_chuck_thermo_energy_mode(self, mode: str) -> Response: Raises: ValueError: If the provided mode is not valid. """ - self.comm.send(f"status:set_chuck_thermo_energy_mode {mode}") + if not isinstance(mode, ChuckThermoEnergyMode): + mode = ChuckThermoEnergyMode[mode] + self.comm.send(f"status:set_chuck_thermo_energy_mode {mode.toSentioAbbr()}") return Response.check_resp(self.comm.read_line()) - def set_chuck_thermo_hold_mode(self, mode: bool) -> Response: + def set_chuck_thermo_hold_mode(self, mode: Union[bool, ChuckThermoHoldMode, str]) -> Response: """Set thermo chuck hold mode. Args: @@ -176,7 +182,11 @@ def set_chuck_thermo_hold_mode(self, mode: bool) -> Response: Returns: A Response object confirming the command execution. """ - self.comm.send(f"status:set_chuck_thermo_hold_mode {mode}") + if isinstance(mode, bool): + mode = ChuckThermoHoldMode.Active if mode else ChuckThermoHoldMode.Nonactive + elif isinstance(mode, str): + mode = ChuckThermoHoldMode[mode] + self.comm.send(f"status:set_chuck_thermo_hold_mode {mode.toSentioAbbr()}") return Response.check_resp(self.comm.read_line()) def set_chuck_thermo_mode(self, mode: str) -> Response: @@ -194,7 +204,7 @@ def set_chuck_thermo_mode(self, mode: str) -> Response: self.comm.send(f"status:set_chuck_thermo_mode {mode}") return Response.check_resp(self.comm.read_line()) - def set_high_purge(self, enable: bool) -> Response: + def set_high_purge(self, enable: Union[bool, HighPurgeState, str]) -> Response: """Set thermo chuck high purge state. Args: @@ -203,5 +213,9 @@ def set_high_purge(self, enable: bool) -> Response: Returns: A Response object confirming the command execution. """ - self.comm.send(f"status:set_high_purge {enable}") + if isinstance(enable, bool): + enable = HighPurgeState.On if enable else HighPurgeState.Off + elif isinstance(enable, str): + enable = HighPurgeState[enable] + self.comm.send(f"status:set_high_purge {enable.toSentioAbbr()}") return Response.check_resp(self.comm.read_line()) \ No newline at end of file diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 1974945..b30d8bf 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -1592,3 +1592,40 @@ def toSentioAbbr(self): FiberType.Lensed: "Lensed", } return switcher.get(self, "Invalid fiber type enumerator") + +class ChuckThermoEnergyMode(Enum): + Fast = 0 + Optimal = 1 + HighPower = 2 + Customized = 3 + + def toSentioAbbr(self): + switcher = { + ChuckThermoEnergyMode.Fast: "Fast", + ChuckThermoEnergyMode.Optimal: "Optimal", + ChuckThermoEnergyMode.HighPower: "HighPower", + ChuckThermoEnergyMode.Customized: "Customized", + } + return switcher.get(self, "Invalid ChuckThermoEnergyMode") + +class ChuckThermoHoldMode(Enum): + Active = 0 + Nonactive = 1 + + def toSentioAbbr(self): + switcher = { + ChuckThermoHoldMode.Active: "Active", + ChuckThermoHoldMode.Nonactive: "Nonactive", + } + return switcher.get(self, "Invalid ChuckThermoHoldMode") + +class HighPurgeState(Enum): + On = 0 + Off = 1 + + def toSentioAbbr(self): + switcher = { + HighPurgeState.On: "ON", + HighPurgeState.Off: "OFF", + } + return switcher.get(self, "Invalid HighPurgeState") diff --git a/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py index a71a7f0..2a13b69 100644 --- a/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py @@ -1,94 +1,95 @@ import unittest -from unittest.mock import MagicMock, patch - +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.Enumerations import LoaderStation, OrientationMarker, WaferStatusItem, RemoteCommandError +from sentio_prober_control.Sentio.ProberBase import ProberException +from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup import LoaderCommandGroup -# 🧰 小工具:建立有 toSentioAbbr() 的 mock enum -def fake_enum(value: str): - mock_enum = MagicMock() - mock_enum.toSentioAbbr.return_value = value - return mock_enum - - -def mock_response(mock_check, message): - mock_resp = MagicMock() - mock_resp.message.return_value = message - mock_check.return_value = mock_resp - - class TestLoaderCommandGroup(unittest.TestCase): + def setUp(self): - self.mock_parent = MagicMock() self.mock_comm = MagicMock() + self.mock_parent = MagicMock() self.mock_parent.comm = self.mock_comm self.loader = LoaderCommandGroup(self.mock_parent) - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_has_station(self, mock_check): - mock_response(mock_check, "1") - station = fake_enum("cas1") - result = self.loader.has_station(station) + def mock_response(self, message="OK"): + mock_resp = MagicMock() + mock_resp.message.return_value = message + return mock_resp + + def test_has_station_true(self): + self.mock_comm.read_line.return_value = "0,0,1" + Response.check_resp = MagicMock(return_value=self.mock_response("1")) + result = self.loader.has_station(LoaderStation.Cassette1) self.assertTrue(result) - self.mock_comm.send.assert_called_with("loader:has_station cas1") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_load_wafer(self, mock_check): - mock_response(mock_check, "OK") - station = fake_enum("cas1") - self.loader.load_wafer(station, 1, 180) - self.mock_comm.send.assert_called_with("loader:load_wafer cas1, 1, 180") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_prealign(self, mock_check): - mock_response(mock_check, "OK") - marker = fake_enum("Notch") - self.loader.prealign(marker, 90) - self.mock_comm.send.assert_called_with("loader:prealign Notch, 90") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_query_wafer_status(self, mock_check): - mock_response(mock_check, "Cassette1,1,200,90,50.0") - station = fake_enum("Cassette1") - result = self.loader.query_wafer_status(station, 1) - self.assertEqual(result, ("Cassette1", 1, 200, 90, 50.0)) - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_set_wafer_status(self, mock_check): - mock_response(mock_check, "OK") - station = fake_enum("cas1") - status_item = fake_enum("Orientation") - self.loader.set_wafer_status(station, 2, status_item, 180) - self.mock_comm.send.assert_called_with("loader:set_wafer_status cas1,2,Orientation,180") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_set_wafer_id(self, mock_check): - mock_response(mock_check, "OK") - station = fake_enum("cas1") - self.loader.set_wafer_id(station, 1, "wafer_123") - self.mock_comm.send.assert_called_with("loader:set_wafer_id cas1, 1, wafer_123") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_query_wafer_id(self, mock_check): - mock_response(mock_check, "wafer_123") - station = fake_enum("cas1") - self.loader.query_wafer_id(station, 1) - self.mock_comm.send.assert_called_with("loader:query_wafer_id cas1, 1") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_start_prepare_wafer(self, mock_check): - mock_response(mock_check, "OK") - s1 = fake_enum("cas1") - s2 = fake_enum("cas2") - self.loader.start_prepare_wafer(s1, 1, 0, 1, s2, 2) - self.mock_comm.send.assert_called_with("loader:start_prepare_wafer cas1, 1, 0, 1, cas2, 2") - - @patch("sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup.Response.check_resp") - def test_query_station_status(self, mock_check): - mock_response(mock_check, "000111") - station = fake_enum("cas1") - self.loader.query_station_status(station) - self.mock_comm.send.assert_called_with("loader:query_station_status cas1") - - -if __name__ == "__main__": - unittest.main() + + def test_scan_station_returns_string(self): + self.mock_comm.read_line.return_value = "0,0,data" + Response.check_resp = MagicMock(return_value=self.mock_response("data")) + result = self.loader.scan_station(LoaderStation.Cassette1) + self.assertEqual(result, "data") + + def test_load_wafer_with_angle(self): + self.mock_comm.read_line.return_value = "0,0,OK" + Response.check_resp = MagicMock(return_value=self.mock_response("OK")) + self.loader.load_wafer(LoaderStation.WaferWallet, 1, 90) + self.mock_comm.send.assert_called_with("loader:load_wafer ww, 1, 90") + + def test_load_wafer_without_angle(self): + self.mock_comm.read_line.return_value = "0,0,OK" + Response.check_resp = MagicMock(return_value=self.mock_response("OK")) + self.loader.load_wafer(LoaderStation.WaferWallet, 1) + self.mock_comm.send.assert_called_with("loader:load_wafer ww, 1") + + def test_query_wafer_status_returns_tuple(self): + self.mock_comm.read_line.return_value = "0,0,Cassette1,1,200,180,90" + Response.check_resp = MagicMock(return_value=self.mock_response("Cassette1,1,200,180,90")) + result = self.loader.query_wafer_status(LoaderStation.Cassette1, 1) + self.assertEqual(result[0], LoaderStation.Cassette1) + self.assertEqual(result[1], 1) + self.assertEqual(result[2], 200) + self.assertEqual(result[3], 180) + self.assertEqual(result[4], 90.0) + + def test_query_wafer_status_returns_none(self): + class FakeProberException(ProberException): # ← 繼承 ProberException + def __init__(self, err): + self._err = err + + def error(self): + return self._err + + def raise_empty(*args, **kwargs): + raise FakeProberException(RemoteCommandError.SlotOrStationEmpty) + + Response.check_resp = MagicMock(side_effect=raise_empty) + result = self.loader.query_wafer_status(LoaderStation.Cassette1, 1) + self.assertIsNone(result) + + def test_prealign(self): + self.mock_comm.read_line.return_value = "0,0,OK" + Response.check_resp = MagicMock(return_value=self.mock_response("OK")) + self.loader.prealign(OrientationMarker.Notch, 180) + self.mock_comm.send.assert_called_with("loader:prealign Notch, 180") + + def test_set_wafer_status(self): + self.mock_comm.read_line.return_value = "0,0,OK" + Response.check_resp = MagicMock(return_value=self.mock_response("OK")) + self.loader.set_wafer_status(LoaderStation.Cassette1, 5, WaferStatusItem.Progress, 80.0) + self.mock_comm.send.assert_called_with("loader:set_wafer_status cas1,5,Progress,80.0") + + def test_transfer_wafer(self): + self.mock_comm.read_line.return_value = "0,0,OK" + Response.check_resp = MagicMock(return_value=self.mock_response("OK")) + self.loader.transfer_wafer(LoaderStation.Cassette1, 1, LoaderStation.Chuck, 1) + self.mock_comm.send.assert_called_with("loader:transfer_wafer cas1, 1, chuck, 1") + + def test_unload_wafer(self): + self.mock_comm.read_line.return_value = "0,0,OK" + Response.check_resp = MagicMock(return_value=self.mock_response("OK")) + self.loader.unload_wafer() + self.mock_comm.send.assert_called_with("loader:unload_wafer") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/sentio_prober_control/UnitTest/TestStatusCommandGroup.py b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py index e25c005..02ef29f 100644 --- a/sentio_prober_control/UnitTest/TestStatusCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py @@ -1,101 +1,91 @@ import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.Enumerations import ThermoChuckState, ChuckThermoEnergyMode, ChuckThermoHoldMode, HighPurgeState from sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup import StatusCommandGroup -from sentio_prober_control.Sentio.Enumerations import ThermoChuckState - +from sentio_prober_control.Sentio.Response import Response class TestStatusCommandGroup(unittest.TestCase): def setUp(self): - self.mock_parent = MagicMock() self.mock_comm = MagicMock() + self.mock_parent = MagicMock() self.mock_parent.comm = self.mock_comm - self.status = StatusCommandGroup(self.mock_parent) - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_chuck_temp(self, mock_resp): - mock_resp.return_value.message.return_value = "30.5,OK" - self.assertEqual(self.status.get_chuck_temp(), 30.5) - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_chuck_temp_setpoint(self, mock_resp): - mock_resp.return_value.message.return_value = "25.0,OK" - self.assertEqual(self.status.get_chuck_temp_setpoint(), 25.0) - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_chuck_thermo_state(self, mock_resp): - cases = { - "soaking": ThermoChuckState.Soaking, - "cooling": ThermoChuckState.Cooling, - "heating": ThermoChuckState.Heating, - "controlling": ThermoChuckState.Controlling, - "standby": ThermoChuckState.Standby, - "error": ThermoChuckState.Error, - "uncontrolled": ThermoChuckState.Uncontrolled, - "unknown stuff": ThermoChuckState.Unknown, - } - for msg, expected in cases.items(): - mock_resp.return_value.message.return_value = msg - self.assertEqual(self.status.get_chuck_thermo_state(), expected) - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_machine_status(self, mock_resp): - mock_resp.return_value.message.return_value = "0,0,Ready,IsMeasuring,LoaderBusy" - self.assertEqual(self.status.get_machine_status(), (True, True, True)) - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_soaking_time(self, mock_resp): - mock_resp.return_value.message.return_value = "120.0" - self.assertEqual(self.status.get_soaking_time(80.0), 120.0) - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_set_chuck_temp(self, mock_resp): - mock_resp.return_value.message.return_value = "OK" - resp = self.status.set_chuck_temp(75.0) - self.assertEqual(resp.message(), "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_chuck_thermo_energy_mode(self, mock_resp): - mock_resp.return_value.message.return_value = "Fast" - self.assertEqual(self.status.get_chuck_thermo_energy_mode(), "Fast") - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_chuck_thermo_hold_mode(self, mock_resp): - mock_resp.return_value.message.return_value = "Active" - self.assertEqual(self.status.get_chuck_thermo_hold_mode(), "Active") - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_get_high_purge_state(self, mock_resp): - mock_resp.return_value.message.return_value = "ON" - self.assertEqual(self.status.get_high_purge_state(), "ON") - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_set_chuck_thermo_energy_mode(self, mock_resp): - mock_resp.return_value.message.return_value = "OK" - resp = self.status.set_chuck_thermo_energy_mode("Optimal") - self.assertEqual(resp.message(), "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_set_chuck_thermo_hold_mode(self, mock_resp): - mock_resp.return_value.message.return_value = "OK" - self.status.comm.send = MagicMock() - resp = self.status.set_chuck_thermo_hold_mode(True) - self.status.comm.send.assert_called_with("status:set_chuck_thermo_hold_mode True") - self.assertEqual(resp.message(), "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_set_chuck_thermo_mode(self, mock_resp): - mock_resp.return_value.message.return_value = "OK" - resp = self.status.set_chuck_thermo_mode("Turbo") - self.assertEqual(resp.message(), "OK") - - @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") - def test_set_high_purge(self, mock_resp): - mock_resp.return_value.message.return_value = "OK" - self.status.comm.send = MagicMock() - resp = self.status.set_high_purge(True) - self.status.comm.send.assert_called_with("status:set_high_purge True") - self.assertEqual(resp.message(), "OK") - - -if __name__ == "__main__": - unittest.main() + self.cmd = StatusCommandGroup(self.mock_parent) + + def mock_response(self, message="OK"): + mock_resp = MagicMock() + mock_resp.message.return_value = message + return mock_resp + + def test_get_chuck_temp(self): + self.mock_comm.read_line.return_value = "0,0,25.0" + Response.check_resp = MagicMock(return_value=self.mock_response("25.0")) + temp = self.cmd.get_chuck_temp() + self.assertEqual(temp, 25.0) + + def test_get_chuck_temp_setpoint(self): + Response.check_resp = MagicMock(return_value=self.mock_response("30.0")) + temp = self.cmd.get_chuck_temp_setpoint() + self.assertEqual(temp, 30.0) + + def test_get_chuck_thermo_state(self): + Response.check_resp = MagicMock(return_value=self.mock_response("cooling")) + state = self.cmd.get_chuck_thermo_state() + self.assertEqual(state, ThermoChuckState.Cooling) + + def test_get_machine_status(self): + Response.check_resp = MagicMock(return_value=self.mock_response("Ready,IsMeasuring,LoaderBusy")) + result = self.cmd.get_machine_status() + self.assertEqual(result, (True, True, True)) + + def test_get_soaking_time(self): + Response.check_resp = MagicMock(return_value=self.mock_response("60.0")) + seconds = self.cmd.get_soaking_time(80.0) + self.assertEqual(seconds, 60.0) + + def test_get_chuck_thermo_energy_mode(self): + Response.check_resp = MagicMock(return_value=self.mock_response("Fast")) + result = self.cmd.get_chuck_thermo_energy_mode() + self.assertEqual(result, ChuckThermoEnergyMode.Fast) + + def test_set_chuck_thermo_energy_mode_enum(self): + Response.check_resp = MagicMock(return_value=self.mock_response()) + resp = self.cmd.set_chuck_thermo_energy_mode(ChuckThermoEnergyMode.Optimal) + self.assertTrue(resp.message(), "OK") + + def test_set_chuck_thermo_energy_mode_str(self): + Response.check_resp = MagicMock(return_value=self.mock_response()) + resp = self.cmd.set_chuck_thermo_energy_mode("HighPower") + self.assertTrue(resp.message(), "OK") + + def test_get_chuck_thermo_hold_mode(self): + Response.check_resp = MagicMock(return_value=self.mock_response("Active")) + result = self.cmd.get_chuck_thermo_hold_mode() + self.assertEqual(result, ChuckThermoHoldMode.Active) + + def test_set_chuck_thermo_hold_mode_bool(self): + Response.check_resp = MagicMock(return_value=self.mock_response()) + resp = self.cmd.set_chuck_thermo_hold_mode(True) + self.assertTrue(resp.message(), "OK") + + def test_set_chuck_thermo_hold_mode_enum(self): + Response.check_resp = MagicMock(return_value=self.mock_response()) + resp = self.cmd.set_chuck_thermo_hold_mode(ChuckThermoHoldMode.Nonactive) + self.assertTrue(resp.message(), "OK") + + def test_get_high_purge_state(self): + Response.check_resp = MagicMock(return_value=self.mock_response("On")) + result = self.cmd.get_high_purge_state() + self.assertEqual(result, HighPurgeState.On) + + def test_set_high_purge_bool(self): + Response.check_resp = MagicMock(return_value=self.mock_response()) + resp = self.cmd.set_high_purge(False) + self.assertTrue(resp.message(), "OK") + + def test_set_high_purge_enum(self): + Response.check_resp = MagicMock(return_value=self.mock_response()) + resp = self.cmd.set_high_purge(HighPurgeState.On) + self.assertTrue(resp.message(), "OK") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 62e87fb8a6fd372204945181e549ef363c1b4be2 Mon Sep 17 00:00:00 2001 From: Hoca Chen Date: Tue, 1 Apr 2025 10:22:48 +0800 Subject: [PATCH 06/41] update the command follow the rule --- .../Sentio/CommandGroups/ProbeCommandGroup.py | 2 +- .../Sentio/CommandGroups/SiPHCommandGroup.py | 2 +- sentio_prober_control/Sentio/ProberSentio.py | 38 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py index dd81592..a0c5d3c 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, float, float, str]: + def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[str, float, float, str]: """Get information for a probe site. Each positioner can define n a number of predefined positions called "sites". diff --git a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py index 2df8301..8949c02 100644 --- a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py @@ -137,7 +137,7 @@ def move_origin(self, probe: ProbeSentio) -> None: 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()) + 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. diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 4d25601..fc914ff 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -110,7 +110,7 @@ def abort_command(self, cmd_id: int) -> Response: return Response.check_resp(self.comm.read_line()) - def clear_contact(self, site: ChuckSite | None = None) -> Response: + def clear_contact(self, site: ChuckSite | None = None) -> None: """Clear contact positions. Args: @@ -125,7 +125,7 @@ def clear_contact(self, site: ChuckSite | None = None) -> Response: else: self.comm.send(f"clear_contact {site.toSentioAbbr()}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) @staticmethod @@ -150,7 +150,7 @@ def create_prober(comm_type : str | SentioCommunicationType, arg1 : str | GpibCa raise ValueError(f'Unknown prober type: "{comm_type}"') - def enable_chuck_overtravel(self, stat: bool) -> Response: + def enable_chuck_overtravel(self, stat: bool) -> None: """Enable chuck overtravel. This function wraps SENTIO's "enable_chuck_overtravel" remote command. @@ -163,10 +163,10 @@ def enable_chuck_overtravel(self, stat: bool) -> Response: """ self.comm.send(f"enable_chuck_overtravel {stat}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def enable_chuck_hover(self, stat: bool) -> Response: + def enable_chuck_hover(self, stat: bool) -> None: """Enable chuck hover height. The Hover height is a height that is significantly closer to the chuck compared to the separation height. @@ -183,10 +183,10 @@ def enable_chuck_hover(self, stat: bool) -> Response: """ self.comm.send(f"enable_chuck_hover {stat}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> Response: + def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> None: """Enable chuck site hover height. The Hover height is a height that is significantly closer to the chuck compared to the separation height. @@ -202,10 +202,10 @@ def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> Response: """ self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> Response: + def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> None: """Enable overtravel distance for a specific chuck site. Args: @@ -217,7 +217,7 @@ def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> Response: """ self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def file_transfer(self, source: str, dest: str) -> Response: @@ -551,7 +551,7 @@ def move_chuck_home(self) -> Tuple[float, float]: return float(tok[0]), float(tok[1]) - def move_chuck_load(self, pos: LoadPosition) -> Response: + def move_chuck_load(self, pos: LoadPosition) -> None: """Move chuck to load position. Wraps SENTIO's "move_chuck_load" remote command. @@ -563,7 +563,7 @@ def move_chuck_load(self, pos: LoadPosition) -> Response: A response object with the result of the command. """ self.comm.send(f"move_chuck_load {pos.toSentioAbbr()}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def move_chuck_separation(self) -> float: @@ -644,7 +644,7 @@ def move_chuck_z(self, ref: ChuckZReference, z: float) -> float: resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) - def move_chuck_work_area(self, area: WorkArea) -> Response: + def move_chuck_work_area(self, area: WorkArea) -> None: """Move the chuck to a given work area. A SENTIO probe station can have different work areas. One area is for probing. This is the default @@ -663,7 +663,7 @@ def move_chuck_work_area(self, area: WorkArea) -> Response: A response object with the result of the command. """ self.comm.send(f"move_chuck_work_area {area.toSentioAbbr()}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def move_scope_xy( self, ref: ScopeXYReference, x: float, y: float @@ -686,7 +686,7 @@ def move_scope_xy( tok = resp.message().split(",") return float(tok[0]), float(tok[1]) - def move_scope_lift(self, state: bool): + def move_scope_lift(self, state: bool) -> None: """Move scope to its lift position. The scope lift position is a position where the scope is @@ -741,7 +741,7 @@ def name(self) -> str: """ return self.__name - def open_project(self, project: str, restore_heights: bool = False): + def open_project(self, project: str, restore_heights: bool = False) -> None: """Open a SENTIO project file. Wraps SENTIO's "open_project" remote command. @@ -805,7 +805,7 @@ def save_project(self, project: str): Response.check_resp(self.comm.read_line()) - def select_module(self, module: Module, tabSheet: str | None = None): + def select_module(self, module: Module, tabSheet: str | None = None) -> None: """Activate a given SENTIO module. In response to this function SENTIO will switch its user interface to make @@ -904,7 +904,7 @@ def set_stepping_contact_mode(self, mode: SteppingContactMode) -> None: Response.check_resp(self.comm.read_line()) - def set_vacuum(self, site: ChuckSite, stat: bool) -> Response: + def set_vacuum(self, site: ChuckSite, stat: bool) -> None: """Switches the vacuum of a chuck site on or off. Args: @@ -916,7 +916,7 @@ def set_vacuum(self, site: ChuckSite, stat: bool) -> Response: """ self.comm.send(f"set_vacuum {site.toSentioAbbr()}, {stat}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def show_hint(self, msg: str, subtext: str) -> Response: From 87d90bf333524d0676a4a481adec0a44fb2fbf27 Mon Sep 17 00:00:00 2001 From: Cynthia Date: Tue, 1 Apr 2025 14:11:50 +0800 Subject: [PATCH 07/41] Added map cmd & unit test --- .../CommandGroups/WafermapBinsCommandGroup.py | 68 ++++-- .../CommandGroups/WafermapCommandGroup.py | 124 +++++++--- .../WafermapCompensationCommandGroup.py | 10 +- .../CommandGroups/WafermapPathCommandGroup.py | 26 ++- .../CommandGroups/WafermapPoiCommandGroup.py | 34 ++- .../WafermapSubsiteCommandGroup.py | 34 ++- sentio_prober_control/Sentio/Enumerations.py | 140 ++++++++++- .../UnitTest/TestWafermapBinsCommandGroup.py | 88 +++++++ .../UnitTest/TestWafermapCommandGroup.py | 218 ++++++++++++++++++ .../TestWafermapCompensationCommandGroup.py | 11 +- .../UnitTest/TestWafermapPathCommandGroup.py | 40 +++- .../UnitTest/TestWafermapPoiCommandGroup.py | 26 ++- .../TestWafermapSubsiteCommandGroup.py | 12 + 13 files changed, 734 insertions(+), 97 deletions(-) create mode 100644 sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestWafermapCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapBinsCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapBinsCommandGroup.py index c7da1f8..7c45dd3 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapBinsCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapBinsCommandGroup.py @@ -9,11 +9,10 @@ class WafermapBinsCommandGroup(CommandGroupBase): """This command group bundles functions for setting up and using the binning table of the wafermap.""" def clear_all(self) -> None: - """Clear all bins. Remove the bin code from all dies and sibsites.""" + """Clear all bins. Remove the bin code from all dies and subsites.""" self.comm.send("map:bins:clear_all") Response.check_resp(self.comm.read_line()) - def clear_all_values(self) -> None: """Removes all temporarily stored values from the dies. @@ -23,7 +22,6 @@ def clear_all_values(self) -> None: self.comm.send("map:bins:clear_all_values") Response.check_resp(self.comm.read_line()) - def get_bin(self, col: int | None = None, row: int | None = None, site: int | None = None) -> int: """Get the bin information of a die or a subsite. @@ -38,43 +36,41 @@ def get_bin(self, col: int | None = None, row: int | None = None, site: int | No The bin value of the die or subsite. """ if col is None and row is None and site is None: - self.comm.send(f"map:bins:get_bin") + self.comm.send("map:bins:get_bin") elif site is None and col is not None and row is not None: self.comm.send(f"map:bins:get_bin {col}, {row}") elif site is not None and col is not None and row is not None: self.comm.send(f"map:bins:get_bin {col}, {row}, {site}") else: - raise ValueError("get_bin command requires either no parameter or column and row or column, row and site.") - + raise ValueError("get_bin command requires either no parameter, column and row, or column, row, and site.") + resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) - - def get_bin_info(self, bin : int) -> Tuple[int, str, BinQuality, str]: + def get_bin_info(self, bin: int) -> Tuple[int, str, BinQuality, str]: """Get the information of a bin code from the binning table defined in the wafer map. - - Wraps SENTIO's map:bins:get_bin_info remote command. - Returns: - A tuple containing the following data items: bin index (same value as the argument), bin description, bin quality (pass/fail information) and bin color. + Wraps SENTIO's map:bins:get_bin_info remote command. + + Returns: + A tuple containing the following data items: bin index (same value as the argument), bin description, + bin quality (pass/fail information), and bin color. """ - self.comm.send(f"map:bins:get_bin_info {bin}") + self.comm.send(f"map:bins:get_bin_info {bin}") resp = Response.check_resp(self.comm.read_line()) values = resp.message().split(",") return int(values[0]), values[1], BinQuality[values[2]], values[3] - def get_num_bins(self) -> int: """Get the number of bins in the binning table. - - Returns: - The number of bins in the binning table. + + Returns: + The number of bins in the binning table. """ self.comm.send("map:bins:get_num_bins") resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) - def load(self, file: str) -> None: """Load a binning table from file. @@ -87,7 +83,6 @@ def load(self, file: str) -> None: self.comm.send(f"map:bins:load {file}") Response.check_resp(self.comm.read_line()) - def set_all(self, bin_val: int, selection: BinSelection) -> None: """Sets the bins of all dies on the wafermap to a specific value. @@ -98,8 +93,7 @@ def set_all(self, bin_val: int, selection: BinSelection) -> None: selection: The selection of dies to set the bin value for. """ self.comm.send(f"map:bins:set_all {bin_val}, {selection.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) # - + Response.check_resp(self.comm.read_line()) def set_bin(self, bin_value: int, col: int | None = None, row: int | None = None, site: int | None = None) -> None: """Set a single bin. @@ -108,7 +102,7 @@ def set_bin(self, bin_value: int, col: int | None = None, row: int | None = None Args: bin_value: The bin value to set. - col: The column of the die.# + col: The column of the die. row: The row of the die. site: The site of the die. """ @@ -119,11 +113,10 @@ def set_bin(self, bin_value: int, col: int | None = None, row: int | None = None elif site is not None and col is not None and row is not None: self.comm.send(f"map:bins:set_bin {bin_value}, {col}, {row}, {site}") else: - raise ValueError("set_bin command requires either no parameter or column and row or column, row and site.") + raise ValueError("set_bin command requires either no parameter, column and row, or column, row, and site.") Response.check_resp(self.comm.read_line()) - def set_value(self, value: float, col: int, row: int) -> None: """Set a value on a single die. @@ -132,5 +125,30 @@ def set_value(self, value: float, col: int, row: int) -> None: col: The column of the die. row: The row of the die. """ - self.comm.send("map:bins:set_value {0}, {1}, {2}".format(value, col, row)) + self.comm.send(f"map:bins:set_value {value}, {col}, {row}") + Response.check_resp(self.comm.read_line()) + + def set_bin_info(self, index: int, description: str, bin_quality: BinQuality, color: str) -> None: + """Set bin information in the binning table. + + Wraps SENTIO's map:bins:set_bin_info remote command. + + Args: + index: The index of the bin. + description: The description of the bin. + bin_quality: Pass/Fail/Undefined. + color: The color of the bin. + """ + self.comm.send(f"map:bins:set_bin_info {index}, {description}, {bin_quality.toSentioAbbr()}, {color}") Response.check_resp(self.comm.read_line()) + + def resize(self, bin_table_size: int) -> None: + """Resize the binning table. + + Wraps SENTIO's map:bins:resize remote command. + + Args: + bin_table_size: The new size of the binning table. + """ + self.comm.send(f"map:bins:resize {bin_table_size}") + Response.check_resp(self.comm.read_line()) \ No newline at end of file diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py index 11bd8af..74e3240 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py @@ -1,6 +1,7 @@ from typing import Tuple -from sentio_prober_control.Sentio.Enumerations import AxisOrient, ColorScheme, DieNumber, StatusBits +from sentio_prober_control.Sentio.Enumerations import AxisOrient, ColorScheme, DieNumber, StatusBits, RoutingStartPoint, \ + RoutingPriority from sentio_prober_control.Sentio.ProberBase import ProberException from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ModuleCommandGroupBase @@ -10,6 +11,7 @@ from sentio_prober_control.Sentio.CommandGroups.WafermapPathCommandGroup import WafermapPathCommandGroup from sentio_prober_control.Sentio.CommandGroups.WafermapPoiCommandGroup import WafermapPoiCommandGroup from sentio_prober_control.Sentio.CommandGroups.WafermapSubsiteCommandGroup import WafermapSubsiteGroup +from sentio_prober_control.Sentio.CommandGroups.WafermapViewCommandGroup import WafermapViewCommandGroup class WafermapCommandGroup(ModuleCommandGroupBase): @@ -46,7 +48,7 @@ def __init__(self, comm) -> None: self.path: WafermapPathCommandGroup = WafermapPathCommandGroup(comm) self.poi: WafermapPoiCommandGroup = WafermapPoiCommandGroup(comm) self.subsites: WafermapSubsiteGroup = WafermapSubsiteGroup(comm, self) - + self.view: WafermapViewCommandGroup = WafermapViewCommandGroup(comm) def bin_step_next_die(self, bin_value: int, site: int | None = None) -> Tuple[int, int, int]: """Bin the current die and step to the naxt die. @@ -81,7 +83,6 @@ def bin_step_next_die(self, bin_value: int, site: int | None = None) -> Tuple[in tok = resp.message().split(",") return int(tok[0]), int(tok[1]), int(tok[2]) - def create(self, diameter: float) -> None: """Create a new round wafer map. @@ -94,7 +95,6 @@ def create(self, diameter: float) -> None: self.comm.send(f"map:create {diameter}") Response.check_resp(self.comm.read_line()) - def create_rect(self, cols: int, rows: int) -> None: """Create a new rectangular wafer map. @@ -108,7 +108,6 @@ def create_rect(self, cols: int, rows: int) -> None: self.comm.send("map:create_rect {0}, {1}".format(cols, rows)) Response.check_resp(self.comm.read_line()) - def die_reference_is_set(self) -> bool: """Returns true if the die reference offset is set. @@ -139,7 +138,6 @@ def die_reference_is_set(self) -> bool: resp = Response.check_resp(self.comm.read_line()) return resp.message().lower() == "true" - def end_of_route(self) -> bool: """Returns True if the last stepping command reached the end of the route. @@ -154,7 +152,6 @@ def end_of_route(self) -> bool: """ return self.__end_of_route - def get_axis_orient(self) -> AxisOrient: """Get axis orientation of the wafer map. @@ -181,7 +178,6 @@ def get_axis_orient(self) -> AxisOrient: raise ProberException(f"Unknown axis orientation: {resp.message()}") - def get_diameter(self) -> float: """Get diameter of the wafer map im millimeter. @@ -196,7 +192,6 @@ def get_diameter(self) -> float: dia = int(resp.message()) return dia - def get_die_reference(self) -> Tuple[float, float]: """Get the die reference offset. @@ -229,7 +224,6 @@ def get_die_reference(self) -> Tuple[float, float]: tok = resp.message().split(",") return float(tok[0]), float(tok[1]) - def get_die_seq(self) -> int: """Returns the sequence number of the current die. @@ -248,7 +242,6 @@ def get_die_seq(self) -> int: resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) # 0:Result+status, 1:Command ID, 2:Response - def get_grid_origin(self) -> Tuple[int, int]: """Get origin of the wafermap grid. @@ -263,7 +256,6 @@ def get_grid_origin(self) -> Tuple[int, int]: return int(tok[0]), int(tok[1]) - def get_index_size(self) -> Tuple[float, float]: """Return the die size set up in the wafer map. @@ -275,7 +267,6 @@ def get_index_size(self) -> Tuple[float, float]: tok = resp.message().split(",") return float(tok[0]), float(tok[1]) - def get_num_dies(self, selection: DieNumber) -> int: """Returns the number of dies in the wafer map. @@ -285,7 +276,7 @@ def get_num_dies(self, selection: DieNumber) -> int: Returns: The number of dies. """ - switcher = {DieNumber.Present: "Present", DieNumber.Selected: "Selected"} + switcher = {DieNumber.Present: "Present", DieNumber.Selected: "Selected", DieNumber.Total: "Total"} what = switcher.get(selection, "Invalid die number selector") @@ -293,7 +284,6 @@ def get_num_dies(self, selection: DieNumber) -> int: resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) - def get_street_size(self) -> Tuple[int, int]: """Returns the street size set up in the wafer map. @@ -310,6 +300,51 @@ def get_street_size(self) -> Tuple[int, int]: return int(tok[0]), int(tok[1]) + def get_grid_params(self) -> Tuple[float, float, float, float, float]: + """Retrieves Information about die grid. + + Returns: + A tuple with the die width and height, the xy offset of grid and edge area size in micrometer. + """ + self.comm.send("map:get_grid_params") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + + return float(tok[0]), float(tok[1]), float(tok[2]), float(tok[3]), float(tok[4]) + + def get_home_die(self) -> Tuple[int, int]: + """Retrieves index coordinates of the home die. + + Returns: + A tuple with the die width and height, the xy offset of grid and edge area size in micrometer. + """ + self.comm.send("map:get_home_die") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + + return int(tok[0]), int(tok[1]) + + def get_num_cols(self) -> int: + """Retrieves the number of columns in the grid. + + Returns: + A tuple with the die width and height, the xy offset of grid and edge area size in micrometer. + """ + self.comm.send("map:get_num_cols") + resp = Response.check_resp(self.comm.read_line()) + + return int(resp.message()) + + def get_num_rows(self) -> int: + """Retrieves the number of rows in the grid. + + Returns: + A tuple with the die width and height, the xy offset of grid and edge area size in micrometer. + """ + self.comm.send("map:get_num_rows") + resp = Response.check_resp(self.comm.read_line()) + + return int(resp.message()) def set_axis_orient(self, orient: AxisOrient) -> None: """Set the acis orientation of the custom coordinate system. @@ -326,7 +361,6 @@ def set_axis_orient(self, orient: AxisOrient) -> None: self.comm.send(f"map:set_axis_orient {orient.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) - def set_color_scheme(self, scheme: ColorScheme) -> None: """Set color scheme of the wafermap. @@ -345,8 +379,7 @@ def set_color_scheme(self, scheme: ColorScheme) -> None: self.comm.send(f"map:set_color_scheme {scheme.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) - - def set_flat_params(self, angle: float, width: float) -> None: + def set_flat_params(self, angle: int, width: int) -> None: """Set the flat parameters of the wafer map. Sets the parameters of the flat wafer orientation marker for @@ -359,7 +392,6 @@ def set_flat_params(self, angle: float, width: float) -> None: self.comm.send("map:set_flat_params {0}, {1}".format(angle, width)) Response.check_resp(self.comm.read_line()) - def set_grid_origin(self, x: int, y: int) -> None: """Set a user defined grid origin. @@ -377,7 +409,6 @@ def set_grid_origin(self, x: int, y: int) -> None: self.comm.send(f"map:set_grid_origin {x}, {y}") Response.check_resp(self.comm.read_line()) - def set_grid_params(self, ix: float, iy: float, offx: float, offy: float, edge: int) -> None: """Set wafermap grid parameters. This function defines the wafermapo grid layout which means setting the size of a die. Setting the grid offset and the size of the edge exclusion zone. @@ -406,7 +437,6 @@ def set_grid_params(self, ix: float, iy: float, offx: float, offy: float, edge: self.comm.send(f"map:set_grid_params {ix}, {iy}, {offx}, {offy}, {edge}") Response.check_resp(self.comm.read_line()) - def set_home_die(self, x: int, y: int) -> None: """ " Sets the home die coordinates in custom coordinates. @@ -419,7 +449,6 @@ def set_home_die(self, x: int, y: int) -> None: self.comm.send(f"map:set_home_die {x}, {y}") Response.check_resp(self.comm.read_line()) - def set_index_size(self, x: float, y: float) -> None: """Set the size of a die. @@ -432,7 +461,6 @@ def set_index_size(self, x: float, y: float) -> None: self.comm.send("map:set_index_size {0}, {1}".format(x, y)) Response.check_resp(self.comm.read_line()) - def set_street_size(self, x: float, y: float) -> None: """Set size of streetlines. @@ -447,7 +475,6 @@ def set_street_size(self, x: float, y: float) -> None: self.comm.send("map:set_street_size {0}, {1}".format(x, y)) Response.check_resp(self.comm.read_line()) - def step_die(self, col: int, row: int, site: int = 0) -> Tuple[int, int, int]: """Step to a specific die (or subsite) which is identified by its column, row and subsite number. @@ -478,7 +505,6 @@ def step_die(self, col: int, row: int, site: int = 0) -> Tuple[int, int, int]: tok = resp.message().split(",") return int(tok[0]), int(tok[1]), int(tok[2]) - def step_die_seq(self, seq: int, site: int) -> Tuple[int, int, int]: """Step to a specific die in the stepping sequence. @@ -502,7 +528,6 @@ def step_die_seq(self, seq: int, site: int) -> Tuple[int, int, int]: return int(tok[0]), int(tok[1]), int(tok[2]) - def step_first_die(self, site: int | None = None) -> Tuple[int, int, int]: """Step to the first die in the stepping sequence. @@ -531,7 +556,6 @@ def step_first_die(self, site: int | None = None) -> Tuple[int, int, int]: return int(tok[0]), int(tok[1]), int(tok[2]) - def step_next_die(self, site: int | None = None) -> Tuple[int, int, int]: """Step to the next die in the stepping sequence. @@ -556,3 +580,49 @@ def step_next_die(self, site: int | None = None) -> Tuple[int, int, int]: tok = resp.message().split(",") return int(tok[0]), int(tok[1]), int(tok[2]) + + def get_orient_marker(self) -> Tuple[str, float, float]: + """Retrieves the type, angle, and size of the wafer orientation marker.""" + self.comm.send("map:get_orient_marker") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return tok[0], float(tok[1]), float(tok[2]) + + def get_routing(self) -> Tuple[RoutingStartPoint, RoutingPriority]: + """Retrieves routing scheme for die stepping.""" + self.comm.send("map:get_routing") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + print(RoutingStartPoint.fromSentioAbbr(tok[0]), RoutingStartPoint.fromSentioAbbr(tok[1])) + return RoutingStartPoint.fromSentioAbbr(tok[0]), RoutingStartPoint.fromSentioAbbr(tok[1]) + + def open(self, file_path: str) -> None: + """Open a wafer map file.""" + self.comm.send(f"map:open {file_path}") + Response.check_resp(self.comm.read_line()) + + def save(self, file_path: str) -> None: + """Save current wafer map to file.""" + self.comm.send(f"map:save {file_path}") + Response.check_resp(self.comm.read_line()) + + def set_diameter(self, diameter: float) -> None: + """Set wafer diameter in millimeter.""" + self.comm.send(f"map:set_diameter {diameter}") + Response.check_resp(self.comm.read_line()) + + def set_orient_marker(self, marker_type: str, angle: float, size: float) -> None: + """Set wafer orientation marker type (Flat/Notch), angle and size in µm.""" + self.comm.send(f"map:set_orient_marker {marker_type},{angle},{size}") + Response.check_resp(self.comm.read_line()) + + def step_previous_die(self) -> Tuple[int, int, int]: + """Step to previous die in the stepping sequence.""" + self.comm.send("map:step_previous_die") + resp = Response.parse_resp(self.comm.read_line()) + self.__end_of_route = (resp.status() & StatusBits.EndOfRoute) == StatusBits.EndOfRoute + if not resp.ok(): + raise ProberException(resp.message(), resp.errc()) + tok = resp.message().split(",") + return int(tok[0]), int(tok[1]), int(tok[2]) + diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py index 6e43085..543e87f 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py @@ -10,7 +10,7 @@ class WafermapCompensationCommandGroup(CommandGroupBase): """This command group bundles functions for setting up and using XY/Z compensation on the wafermap.""" @deprecated("Use vis.compensation.start_execute(CompensationType.Topography, CompensationMode.Vertical) instead") - def topography(self, execute: ExecuteAction) -> int: + def topography(self, execute: ExecuteAction) -> Response: """Execute topography compensation. Args: @@ -25,7 +25,7 @@ def topography(self, execute: ExecuteAction) -> int: if not resp.ok(): raise ProberException(resp.message()) - return resp.cmd_id() + return resp def set_xy(self, comp_type: XyCompensationType) -> None: """Enable the XY Stepping Compensation. @@ -36,11 +36,13 @@ def set_xy(self, comp_type: XyCompensationType) -> None: self.comm.send(f"map:compensation:set_xy {comp_type.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) - def set_z(self, comp_type: ZCompensationType) -> None: + def set_z(self, comp_type: ZCompensationType) -> Response: """Enable the Z Stepping Compensation. Args: comp_type: The type of Z Stepping Compensation. """ self.comm.send(f"map:compensation:set_z {comp_type.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) + resp = Response.check_resp(self.comm.read_line()) + + return resp diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py index d8281a4..07403c8 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py @@ -1,6 +1,6 @@ from typing import Tuple -from sentio_prober_control.Sentio.Enumerations import RoutingPriority, RoutingStartPoint, TestSelection +from sentio_prober_control.Sentio.Enumerations import RoutingPriority, RoutingStartPoint, TestSelection, PathSelection from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase @@ -11,16 +11,23 @@ class WafermapPathCommandGroup(CommandGroupBase): A test path defines which dies are tested in which order. """ - def create_from_bin(self, bin_val: int) -> None: + def create_from_bin(self, bin_val: int | str | PathSelection) -> int: """Create test path by using all dies with a specific bin. Wraps SENTIO's map:path:create_from_bin remote command. Args: - bin_val: The bin value to use. + bin_val: The bin value to use. Can be an int or PathSelection enum. """ - self.comm.send("map:path:create_from_bins {0}".format(bin_val)) - Response.check_resp(self.comm.read_line()) + if isinstance(bin_val, PathSelection): + bin_val_str = bin_val.toSentioAbbr() + else: + bin_val_str = str(bin_val) + + self.comm.send(f"map:path:create_from_bins {bin_val_str}") + resp = Response.check_resp(self.comm.read_line()) + + return int(resp.message()) def get_die(self, seq: int) -> Tuple[int, int]: """Get die column and row coordinates from a sequence number. @@ -62,7 +69,7 @@ def set_routing(self, sp: RoutingStartPoint, pri: RoutingPriority) -> None: self.comm.send(f"map:set_routing {sp.toSentioAbbr()}, {pri.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) - def add_bins(self, selection: str) -> int: + def add_bins(self, selection: int | str | PathSelection) -> int: """Adds dies with specific bin(s) to the stepping path. Wraps SENTIO's map:path:add_bins remote command. @@ -73,7 +80,12 @@ def add_bins(self, selection: str) -> int: Returns: Path length after addition. """ - self.comm.send(f"map:path:add_bins {selection}") + if isinstance(selection, PathSelection): + selection_str = selection.toSentioAbbr() + else: + selection_str = str(selection) + + self.comm.send(f"map:path:add_bins {selection_str}") resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py index 7f88f9f..cb4b025 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py @@ -19,6 +19,22 @@ def add(self, x: float, y: float, desc: str) -> None: self.comm.send(f"map:poi:add {x}, {y}, {desc}") Response.check_resp(self.comm.read_line()) + def get(self, idx: int) -> tuple[float, float, str]: + """Get POI data of a single POI. + + Wraps SENTIO's map:poi:get remote command. + + Args: + idx: The index of the POI to retrieve. + + Returns: + A tuple (x, y, description) of the POI. + """ + self.comm.send(f"map:poi:get {idx}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]), str(tok[2]) + def get_num(self) -> int: """Returns the number of POIs in the list. @@ -45,7 +61,7 @@ def reset(self, stage: Stage, refXy: PoiReferenceXy) -> None: self.comm.send("map:poi:reset {0}, {1}".format(stage.toSentioAbbr(), refXy.toSentioAbbr())) Response.check_resp(self.comm.read_line()) - def step(self, target: str | int) -> None: + def step(self, target: str | int) -> tuple[int, int, int]: """Step to a POI in the list. Wraps SENTIO's map:poi:step remote command. @@ -54,23 +70,29 @@ def step(self, target: str | int) -> None: target: The target POI to step to. This is either the index of the poi or the id of the poi. """ self.comm.send(f"map:poi:step {target}") - Response.check_resp(self.comm.read_line()) + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return int(tok[0]), int(tok[1]), int(tok[2]) - def step_first(self) -> None: + def step_first(self) -> tuple[int, int, int]: """Step to the first POI in the list. Wraps SENTIO's map:poi:step_first remote command. """ self.comm.send("map:poi:step_first") - Response.check_resp(self.comm.read_line()) + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return int(tok[0]), int(tok[1]), int(tok[2]) - def step_next(self) -> None: + def step_next(self) -> tuple[int, int, int]: """Step to the next POI in the list. Wrap SENTIO's map:poi:step_next remote command. """ self.comm.send("map:poi:step_next") - Response.check_resp(self.comm.read_line()) + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return int(tok[0]), int(tok[1]), int(tok[2]) def remove(self, idx: int | None = None) -> None: """Remove POI(s) from wafermap. diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py index b315a77..1b3e844 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapSubsiteCommandGroup.py @@ -1,6 +1,6 @@ from typing import Tuple -from sentio_prober_control.Sentio.Enumerations import AxisOrient, StatusBits +from sentio_prober_control.Sentio.Enumerations import AxisOrient, StatusBits, SubsiteGroup from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase @@ -18,7 +18,7 @@ def __init__(self, comm, wafermap_command_group) -> None: super().__init__(comm) self._parent_command_group = wafermap_command_group - def add(self, id: str, x: float, y: float, orient: AxisOrient = AxisOrient.UpRight) -> None: + def add(self, id: str, x: float, y: float, orient: AxisOrient = AxisOrient.UpRight) -> int: """Add a single subsite to the wafermap. Creates a new subsite definition in SENTIO. The subsite position is defined @@ -34,7 +34,8 @@ def add(self, id: str, x: float, y: float, orient: AxisOrient = AxisOrient.UpRig orient: The axis orientation used fot the submitted values """ self.comm.send("map:subsite:add {}, {}, {}, {}".format(id, x, y, orient.toSentioAbbr())) - Response.check_resp(self.comm.read_line()) + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) def bin_step_next(self, bin: int) -> Tuple[int, int, int]: """Step to the next active subsite and assign bin code to current subsite. @@ -82,7 +83,7 @@ def get(self, idx: int, orient: AxisOrient | None = None) -> Tuple[str, float, f tok = resp.message().split(",") return str(tok[0]), float(tok[1]), float(tok[2]) - def get_num(self) -> int: + def get_num(self, group: SubsiteGroup | None = None) -> int: """Retrieve the number of subsites per die defined in the wafermap. Wraps the "map:subsite:get_num" remote command. @@ -90,7 +91,12 @@ def get_num(self) -> int: Returns: The number of subsites in the wafermap. """ - self.comm.send("map:subsite:get_num") + if group is None: + group_str = "" + else: + group_str = group.toSentioAbbr() + + self.comm.send(f"map:subsite:get_num {group_str}") resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) @@ -102,6 +108,24 @@ def reset(self) -> None: self.comm.send("map:subsite:reset") Response.check_resp(self.comm.read_line()) + def step(self, target: int | str) -> Tuple[int, int, int]: + """Step to a specific subsite on the current die. + + Wraps the "map:subsite:step" remote command. + + Args: + target: The subsite index or ID to step to. + + Returns: + A tuple containing (column, row, subsite) after the step. + """ + self.comm.send(f"map:subsite:step {target}") + resp = Response.check_resp(self.comm.read_line()) + self._parent_command_group.__end_of_route = (resp.status() & StatusBits.EndOfRoute) == StatusBits.EndOfRoute + + tok = resp.message().split(",") + return int(tok[0]), int(tok[1]), int(tok[2]) + def step_next(self) -> Tuple[int, int, int]: """Step to the next active subsite. diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index b30d8bf..abcd789 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -141,13 +141,68 @@ class BinQuality(Enum): def toSentioAbbr(self): switcher = { - BinQuality.Pass: 0, - BinQuality.Fail: 1, - BinQuality.Undefined: 2, + BinQuality.Pass: "pass", + BinQuality.Fail: "fail", + BinQuality.Undefined: "undefined", } return switcher.get(self, "Invalid bin quality identifier") +class PathSelection(Enum): + """An enumerator for defining the path selection state of a die. + + Attributes: + Pass (0): Die is marked as pass and valid for good bin selection. + Fail (1): Die is marked as fail and should be skipped or binned as failed. + Undefined (2): Die has no defined path selection; may be excluded from testing. + Unbinned (3): Die has not yet been assigned to any bin. + """ + + Pass = 0 + Fail = 1 + Undefined = 2 + Unbinned = 3 + + def toSentioAbbr(self): + switcher = { + PathSelection.Pass: "pass", + PathSelection.Fail: "fail", + PathSelection.Undefined: "undefined", + PathSelection.Unbinned: "unbinned", + } + return switcher.get(self, "Invalid path selection identifier") + + +class SubsiteGroup(Enum): + """An enumerator for defining subsite group types for get_num() command. + + Attributes: + Present (0): Subsites present on a specific die. + Selected (1): Subsites selected for routing on a specific die. + GlobalPresent (2): Global subsite table entries. + GlobalSelected (3): Globally selected subsites. + WaferPresent (4): Subsites present on the entire wafer. + WaferSelected (5): Selected subsites on the entire wafer. + """ + Present = 0 + Selected = 1 + GlobalPresent = 2 + GlobalSelected = 3 + WaferPresent = 4 + WaferSelected = 5 + + def toSentioAbbr(self) -> str: + switcher = { + SubsiteGroup.Present: "P", + SubsiteGroup.Selected: "S", + SubsiteGroup.GlobalPresent: "GP", + SubsiteGroup.GlobalSelected: "GS", + SubsiteGroup.WaferPresent: "WP", + SubsiteGroup.WaferSelected: "WS", + } + return switcher.get(self, "Invalid subsite group identifier") + + class CameraMountPoint(Enum): """Available camera mount points. @@ -418,6 +473,56 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid CompensationType") +class XyCompensationType(Enum): + """A list of XY compensation types. + + Attributes: + Disable (0): None + Topography (1): Vertical (Z) compensation. + MapScan (2): Both lateral and vertical compensation. + AlignDie (3): Probe card compensation. + SkateDetection (4): MapScan compensation. + """ + + Disable = 0 + OnTheFly = 1 + MapScan = 2 + Thermal = 3 + + def toSentioAbbr(self): + switcher = { + XyCompensationType.Disable: "None", + XyCompensationType.OnTheFly: "OnTheFly", + XyCompensationType.MapScan: "MapScan", + XyCompensationType.Thermal: "Thermal", + } + return switcher.get(self, "Invalid XyCompensationType") + + +class ZCompensationType(Enum): + """A list of Z compensation types. + + Attributes: + Disable (0): None + Topography (1): Vertical (Z) compensation. + MapScan (2): Both lateral and vertical compensation. + AlignDie (3): Probe card compensation. + SkateDetection (4): MapScan compensation. + """ + + Disable = 0 + OnTheFly = 1 + Topography = 2 + + def toSentioAbbr(self): + switcher = { + ZCompensationType.Disable: "None", + ZCompensationType.OnTheFly: "OnTheFly", + ZCompensationType.Topography: "Topography", + } + return switcher.get(self, "Invalid XyCompensationType") + + class DefaultPattern(Enum): """A list of slots for visual patterns used by SENTIO. @@ -608,6 +713,7 @@ class DieNumber(Enum): Present = 1 Selected = 2 + Total = 3 @deprecated("ExecuteAction is deprecated.") @@ -1334,6 +1440,19 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid RoutingPriority enumerator") + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "R": RoutingPriority.RowUniDir, + "C": RoutingPriority.ColUniDir, + "WR": RoutingPriority.RowBiDir, + "WC": RoutingPriority.ColBiDir, + } + try: + return mapping[abbr.upper()] + except KeyError: + raise ValueError(f"Unknown RoutingPriority abbreviation: {abbr}") + class RoutingStartPoint(Enum): """Defines the starting point for routing (stepping commands). @@ -1347,7 +1466,7 @@ class RoutingStartPoint(Enum): UpperLeft = 0 UpperRight = 1 - LowerLeft = 2 + LowerLeft = 2 LowerRight = 3 def toSentioAbbr(self): @@ -1359,6 +1478,19 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid RoutingStartPoint enumerator") + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "UL": RoutingStartPoint.UpperLeft, + "UR": RoutingStartPoint.UpperRight, + "LL": RoutingStartPoint.LowerLeft, + "LR": RoutingStartPoint.LowerRight, + } + try: + return mapping[abbr.upper()] + except KeyError: + raise ValueError(f"Unknown RoutingStartPoint abbreviation: {abbr}") + class StatusBits: """List of status codes used by SENTIO. diff --git a/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py new file mode 100644 index 0000000..809ec32 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py @@ -0,0 +1,88 @@ +import unittest +from unittest.mock import MagicMock + +from sentio_prober_control.Sentio.Enumerations import BinQuality, BinSelection + +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.ProberSentio import SentioProber + + +class TestWafermapBinsCommandGroup(unittest.TestCase): + def setUp(self): + """Mock the communicator and initialize the test prober.""" + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.test_prober = SentioProber(self.mock_comm) + + def test_clear_all(self): + """Test clearing all bins""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.clear_all() + self.mock_comm.send.assert_called_with("map:bins:clear_all") + + def test_clear_all_values(self): + """Test clearing all bin values""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.clear_all_values() + self.mock_comm.send.assert_called_with("map:bins:clear_all_values") + + def test_get_bin(self): + """Test getting bin information of a die""" + self.mock_comm.read_line.return_value = "0,0,5" + result = self.test_prober.map.bins.get_bin(2, 3) + self.mock_comm.send.assert_called_with("map:bins:get_bin 2, 3") + self.assertEqual(result, 5) + + def test_get_bin_info(self): + """Test getting bin information""" + self.mock_comm.read_line.return_value = "0,0,3,TestBin,Pass,#FF00FFFF" + result = self.test_prober.map.bins.get_bin_info(3) + print(result) + self.mock_comm.send.assert_called_with("map:bins:get_bin_info 3") + self.assertEqual(result, (3, "TestBin", BinQuality.Pass, "#FF00FFFF")) + + def test_get_num_bins(self): + """Test getting the number of bins""" + self.mock_comm.read_line.return_value = "0,0,10" + result = self.test_prober.map.bins.get_num_bins() + self.mock_comm.send.assert_called_with("map:bins:get_num_bins") + self.assertEqual(result, 10) + + def test_load(self): + """Test loading binning table from file""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.load("bin_table.xbt") + self.mock_comm.send.assert_called_with("map:bins:load bin_table.xbt") + + def test_set_all(self): + """Test setting all bins to a specific value""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.set_all(3, BinSelection.DiesOnly) + self.mock_comm.send.assert_called_with("map:bins:set_all 3, d") + + def test_set_bin(self): + """Test setting a specific bin""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.set_bin(2, 4, 5) + self.mock_comm.send.assert_called_with("map:bins:set_bin 2, 4, 5") + + def test_set_value(self): + """Test setting a floating-point value on a die""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.set_value(1.5, 2, 3) + self.mock_comm.send.assert_called_with("map:bins:set_value 1.5, 2, 3") + + def test_resize(self): + """Test resizing the binning table""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.resize(3) + self.mock_comm.send.assert_called_with("map:bins:resize 3") + + def test_set_bin_info(self): + """Test setting bin information""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.map.bins.set_bin_info(5, "GoodBin", BinQuality.Pass, "#FF00FFFF") + self.mock_comm.send.assert_called_with("map:bins:set_bin_info 5, GoodBin, pass, #FF00FFFF") + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestWafermapCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapCommandGroup.py new file mode 100644 index 0000000..9820378 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapCommandGroup.py @@ -0,0 +1,218 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.Enumerations import AxisOrient, ColorScheme, DieNumber, RoutingStartPoint, \ + RoutingPriority + + +class TestWafermapCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.prober = SentioProber(self.mock_comm) + + def test_bin_step_next_die(self): + self.mock_comm.read_line.return_value = "0,0,10,20,0" + result = self.prober.map.bin_step_next_die(1) + self.mock_comm.send.assert_called_with("map:bin_step_next_die 1") + self.assertEqual(result, (10, 20, 0)) + + def test_create(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.create(200.0) + self.mock_comm.send.assert_called_with("map:create 200.0") + + def test_create_rect(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.create_rect(10, 20) + self.mock_comm.send.assert_called_with("map:create_rect 10, 20") + + def test_die_reference_is_set(self): + self.mock_comm.read_line.return_value = "0,0,true" + result = self.prober.map.die_reference_is_set() + self.mock_comm.send.assert_called_with("map:get_prop die_reference_is_set") + self.assertTrue(result) + + def test_get_axis_orient(self): + self.mock_comm.read_line.return_value = "0,0,UL" + result = self.prober.map.get_axis_orient() + self.mock_comm.send.assert_called_with("map:get_axis_orient") + self.assertEqual(result.name, "UpLeft") + + def test_get_diameter(self): + self.mock_comm.read_line.return_value = "0,0,200" + result = self.prober.map.get_diameter() + self.mock_comm.send.assert_called_with("map:get_diameter") + self.assertEqual(result, 200) + + def test_get_die_reference(self): + self.mock_comm.read_line.return_value = "0,0,123.45,678.90" + result = self.prober.map.get_die_reference() + self.mock_comm.send.assert_called_with("map:get_prop die_reference") + self.assertEqual(result, (123.45, 678.9)) + + def test_get_die_seq(self): + self.mock_comm.read_line.return_value = "0,0,12" + result = self.prober.map.get_die_seq() + self.mock_comm.send.assert_called_with("map:get_die_seq") + self.assertEqual(result, 12) + + def test_get_grid_origin(self): + self.mock_comm.read_line.return_value = "0,0,3,5" + result = self.prober.map.get_grid_origin() + self.mock_comm.send.assert_called_with("map:get_grid_origin") + self.assertEqual(result, (3, 5)) + + def test_get_index_size(self): + self.mock_comm.read_line.return_value = "0,0,5000.0,5000.0" + result = self.prober.map.get_index_size() + self.mock_comm.send.assert_called_with("map:get_index_size") + self.assertEqual(result, (5000.0, 5000.0)) + + def test_get_num_dies(self): + self.mock_comm.read_line.return_value = "0,0,150" + result = self.prober.map.get_num_dies(DieNumber.Present) + self.mock_comm.send.assert_called_with("map:get_num_dies Present") + self.assertEqual(result, 150) + + def test_get_street_size(self): + self.mock_comm.read_line.return_value = "0,0,100,100" + result = self.prober.map.get_street_size() + self.mock_comm.send.assert_called_with("map:get_street_size") + self.assertEqual(result, (100, 100)) + + def test_get_grid_params(self): + self.mock_comm.read_line.return_value = "0,0,5000,5000,2500,2500,1000" + result = self.prober.map.get_grid_params() + self.mock_comm.send.assert_called_with("map:get_grid_params") + self.assertEqual(result, (5000.0, 5000.0, 2500.0, 2500.0, 1000.0)) + + def test_get_home_die(self): + self.mock_comm.read_line.return_value = "0,0,5,7" + result = self.prober.map.get_home_die() + self.mock_comm.send.assert_called_with("map:get_home_die") + self.assertEqual(result, (5, 7)) + + def test_get_num_cols(self): + self.mock_comm.read_line.return_value = "0,0,15" + result = self.prober.map.get_num_cols() + self.mock_comm.send.assert_called_with("map:get_num_cols") + self.assertEqual(result, 15) + + def test_get_num_rows(self): + self.mock_comm.read_line.return_value = "0,0,20" + result = self.prober.map.get_num_rows() + self.mock_comm.send.assert_called_with("map:get_num_rows") + self.assertEqual(result, 20) + + def test_set_axis_orient(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_axis_orient(AxisOrient.UpRight) + self.mock_comm.send.assert_called_with("map:set_axis_orient UR") + + def test_set_color_scheme(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_color_scheme(ColorScheme.ColorFromBin) + self.mock_comm.send.assert_called_with("map:set_color_scheme 0") + + def test_set_flat_params(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_flat_params(180, 15000) + self.mock_comm.send.assert_called_with("map:set_flat_params 180, 15000") + + def test_set_grid_origin(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_grid_origin(3, 4) + self.mock_comm.send.assert_called_with("map:set_grid_origin 3, 4") + + def test_set_grid_params(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_grid_params(5000, 5000, 2500, 2500, 1000) + self.mock_comm.send.assert_called_with("map:set_grid_params 5000, 5000, 2500, 2500, 1000") + + def test_set_home_die(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_home_die(5, 6) + self.mock_comm.send.assert_called_with("map:set_home_die 5, 6") + + def test_set_index_size(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_index_size(5000, 5000) + self.mock_comm.send.assert_called_with("map:set_index_size 5000, 5000") + + def test_set_street_size(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_street_size(100, 100) + self.mock_comm.send.assert_called_with("map:set_street_size 100, 100") + + def test_step_die(self): + self.mock_comm.read_line.return_value = "0,0,3,4,0" + result = self.prober.map.step_die(3, 4) + self.mock_comm.send.assert_called_with("map:step_die 3, 4, 0") + self.assertEqual(result, (3, 4, 0)) + + def test_step_die_seq(self): + self.mock_comm.read_line.return_value = "0,0,5,6,0" + result = self.prober.map.step_die_seq(10, 0) + self.mock_comm.send.assert_called_with("map:step_die_seq 10, 0") + self.assertEqual(result, (5, 6, 0)) + + def test_step_first_die(self): + self.mock_comm.read_line.return_value = "0,0,1,2,0" + result = self.prober.map.step_first_die() + self.mock_comm.send.assert_called_with("map:step_first_die") + self.assertEqual(result, (1, 2, 0)) + + def test_step_next_die(self): + self.mock_comm.read_line.return_value = "0,0,2,3,0" + result = self.prober.map.step_next_die() + self.mock_comm.send.assert_called_with("map:step_next_die") + self.assertEqual(result, (2, 3, 0)) + + def test_step_previous_die(self): + self.mock_comm.read_line.return_value = "0,0,4,5,0" + result = self.prober.map.step_previous_die() + self.mock_comm.send.assert_called_with("map:step_previous_die") + self.assertEqual(result, (4, 5, 0)) + + def test_end_of_route_flag(self): + # 手動設置 _WafermapCommandGroup__end_of_route 為 True 測試回傳 + self.prober.map._WafermapCommandGroup__end_of_route = True + self.assertTrue(self.prober.map.end_of_route()) + + def test_open(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.open("C:/path/to/mapfile.xwmf") + self.mock_comm.send.assert_called_with("map:open C:/path/to/mapfile.xwmf") + + def test_save(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.save("C:/path/to/output_map.xwmf") + self.mock_comm.send.assert_called_with("map:save C:/path/to/output_map.xwmf") + + def test_set_diameter(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_diameter(200) + self.mock_comm.send.assert_called_with("map:set_diameter 200") + + def test_get_orient_marker(self): + self.mock_comm.read_line.return_value = "0,0,Flat,90.0,1200.0" + marker = self.prober.map.get_orient_marker() + self.mock_comm.send.assert_called_with("map:get_orient_marker") + self.assertEqual(marker, ("Flat", 90.0, 1200.0)) + + def test_set_orient_marker(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.map.set_orient_marker("Notch", 180.0, 600.0) + self.mock_comm.send.assert_called_with("map:set_orient_marker Notch,180.0,600.0") + + def test_get_routing(self): + self.mock_comm.read_line.return_value = "0,0,UL,R" + routing = self.prober.map.get_routing() + self.mock_comm.send.assert_called_with("map:get_routing") + self.assertEqual(routing[0].name, RoutingStartPoint.UpperLeft) + self.assertEqual(routing[1].name, RoutingPriority.RowUniDir) + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py index 7c57dea..5aeeeaa 100644 --- a/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py @@ -19,9 +19,10 @@ def setUp(self): def test_topography(self): """Test executing topography compensation""" self.mock_comm.read_line.return_value = "0,100,OK" - cmd_id = self.test_prober.map.compensation.topography(ExecuteAction.Execute) + resp = self.test_prober.map.compensation.topography(ExecuteAction.Execute) self.mock_comm.send.assert_called_with("map:compensation:topography execute") - self.assertEqual(cmd_id, 100) + self.assertEqual(resp.cmd_id(), 100) + self.assertEqual(resp.message(), "OK") def test_set_xy_compensation(self): """Test enabling XY compensation""" @@ -31,9 +32,11 @@ def test_set_xy_compensation(self): def test_set_z_compensation(self): """Test enabling Z compensation""" - self.mock_comm.read_line.return_value = "0,0,OK" - self.test_prober.map.compensation.set_z(ZCompensationType.Topography) + self.mock_comm.read_line.return_value = "0,100,OK" + resp = self.test_prober.map.compensation.set_z(ZCompensationType.Topography) self.mock_comm.send.assert_called_with("map:compensation:set_z Topography") + self.assertEqual(resp.cmd_id(), 100) + self.assertEqual(resp.message(), "OK") if __name__ == "__main__": diff --git a/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py index 7531b1a..51e915b 100644 --- a/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py @@ -2,7 +2,8 @@ from unittest.mock import MagicMock from sentio_prober_control.Sentio.ProberSentio import SentioProber from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp -from sentio_prober_control.Sentio.Enumerations import RoutingPriority, RoutingStartPoint, TestSelection, RoutingPriority +from sentio_prober_control.Sentio.Enumerations import RoutingPriority, RoutingStartPoint, TestSelection, \ + RoutingPriority, PathSelection class TestWafermapPathCommandGroup(unittest.TestCase): @@ -10,10 +11,23 @@ def setUp(self): self.mock_comm = MagicMock(spec=CommunicatorTcpIp) self.prober = SentioProber(self.mock_comm) - def test_create_from_bin(self): - self.mock_comm.read_line.return_value = "0,0,OK" - self.prober.map.path.create_from_bin(3) - self.mock_comm.send.assert_called_with("map:path:create_from_bins 3") + def test_create_from_bin_with_int(self): + self.mock_comm.read_line.return_value = "0,0,5" + result = self.prober.map.path.create_from_bin(5) + self.mock_comm.send.assert_called_with("map:path:create_from_bins 5") + self.assertEqual(result, 5) + + def test_create_from_bin_with_str(self): + self.mock_comm.read_line.return_value = "0,0,20" + result = self.prober.map.path.create_from_bin("0-1") + self.mock_comm.send.assert_called_with("map:path:create_from_bins 0-1") + self.assertEqual(result, 20) + + def test_create_from_bin_with_enum(self): + self.mock_comm.read_line.return_value = "0,0,1" + result = self.prober.map.path.create_from_bin(PathSelection.Fail) + self.mock_comm.send.assert_called_with("map:path:create_from_bins fail") + self.assertEqual(result, 1) def test_get_die(self): self.mock_comm.read_line.return_value = "0,0,10,20" @@ -31,13 +45,25 @@ def test_set_routing(self): self.prober.map.path.set_routing(RoutingStartPoint.UpperLeft, RoutingPriority.RowUniDir) self.mock_comm.send.assert_called_with("map:set_routing ul, r") - def test_add_bins(self): + def test_add_bins_str(self): self.mock_comm.read_line.return_value = "0,0,12" result = self.prober.map.path.add_bins("1-5") self.mock_comm.send.assert_called_with("map:path:add_bins 1-5") self.assertEqual(result, 12) - def test_remove_bins(self): + def test_add_bins_int(self): + self.mock_comm.read_line.return_value = "0,0,5" + result = self.prober.map.path.add_bins(0) + self.mock_comm.send.assert_called_with("map:path:add_bins 0") + self.assertEqual(result, 5) + + def test_add_bins_enum(self): + self.mock_comm.read_line.return_value = "0,0,20" + result = self.prober.map.path.add_bins(PathSelection.Pass) + self.mock_comm.send.assert_called_with("map:path:add_bins pass") + self.assertEqual(result, 20) + + def test_remove_enum(self): self.mock_comm.read_line.return_value = "0,0,6" result = self.prober.map.path.remove_bins("Fail") self.mock_comm.send.assert_called_with("map:path:remove_bins Fail") diff --git a/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py index e8ca7f6..fee7022 100644 --- a/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py @@ -15,6 +15,12 @@ def test_add(self): self.prober.map.poi.add(100.0, 200.0, "MyPOI") self.mock_comm.send.assert_called_with("map:poi:add 100.0, 200.0, MyPOI") + def test_poi_get(self): + self.mock_comm.read_line.return_value = "0,0,123.45,678.90,MyPOI" + result = self.prober.map.poi.get(2) + self.mock_comm.send.assert_called_with("map:poi:get 2") + self.assertEqual(result, (123.45, 678.90, "MyPOI")) + def test_get_num(self): self.mock_comm.read_line.return_value = "0,0,5" result = self.prober.map.poi.get_num() @@ -27,24 +33,28 @@ def test_reset(self): self.mock_comm.send.assert_called_with("map:poi:reset chuck, DieCenter") def test_step_by_index(self): - self.mock_comm.read_line.return_value = "0,0,OK" - self.prober.map.poi.step(2) + self.mock_comm.read_line.return_value = "0,0,0,1,2" + result = self.prober.map.poi.step(2) self.mock_comm.send.assert_called_with("map:poi:step 2") + self.assertEqual(result, (0, 1, 2)) def test_step_by_id(self): - self.mock_comm.read_line.return_value = "0,0,OK" - self.prober.map.poi.step("POI_ID") + self.mock_comm.read_line.return_value = "0,0,4,3,6" + result = self.prober.map.poi.step("POI_ID") self.mock_comm.send.assert_called_with("map:poi:step POI_ID") + self.assertEqual(result, (4, 3, 6)) def test_step_first(self): - self.mock_comm.read_line.return_value = "0,0,OK" - self.prober.map.poi.step_first() + self.mock_comm.read_line.return_value = "0,0,2,1,5" + result = self.prober.map.poi.step_first() self.mock_comm.send.assert_called_with("map:poi:step_first") + self.assertEqual(result, (2, 1, 5)) def test_step_next(self): - self.mock_comm.read_line.return_value = "0,0,OK" - self.prober.map.poi.step_next() + self.mock_comm.read_line.return_value = "0,0,-1,-9,0" + result = self.prober.map.poi.step_next() self.mock_comm.send.assert_called_with("map:poi:step_next") + self.assertEqual(result, (-1, -9, 0)) def test_remove_all(self): self.mock_comm.read_line.return_value = "0,0,OK" diff --git a/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py index dbf04e8..a5e1754 100644 --- a/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py @@ -44,6 +44,18 @@ def test_reset(self): self.prober.map.subsites.reset() self.mock_comm.send.assert_called_with("map:subsite:reset") + def test_step_with_index(self): + self.mock_comm.read_line.return_value = "0,0,5,10,1" + result = self.prober.map.subsites.step(1) + self.mock_comm.send.assert_called_with("map:subsite:step 1") + self.assertEqual(result, (5, 10, 1)) + + def test_step_with_id_string(self): + self.mock_comm.read_line.return_value = "0,0,3,7,0" + result = self.prober.map.subsites.step("SubA") + self.mock_comm.send.assert_called_with("map:subsite:step SubA") + self.assertEqual(result, (3, 7, 0)) + def test_step_next(self): self.mock_comm.read_line.return_value = "0,0,8,9,2" result = self.prober.map.subsites.step_next() From 732b882a9478782420342d2b5bda8f8c3870d6c5 Mon Sep 17 00:00:00 2001 From: AdamLin0108 <64577065+AdamLin0108@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:39:22 +0800 Subject: [PATCH 08/41] #18000 and #18087 Simplify AuxCommandGroup and QalibriaCommandFroup API --- .../Sentio/CommandGroups/AuxCommandGroup.py | 300 +++++++++++------- .../CommandGroups/QAlibriaCommandGroup.py | 55 ++-- .../UnitTest/TestAuxCommandGroup.py | 128 ++++---- .../UnitTest/TestQAlibriaCommandGroup.py | 28 +- 4 files changed, 310 insertions(+), 201 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py index f282456..f3309c1 100644 --- a/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py @@ -1,13 +1,78 @@ -from typing import Optional, Tuple, List, Dict, Union -from deprecated import deprecated +from enum import Enum +from typing import Optional, Tuple, List +from dataclasses import dataclass +from sentio_prober_control.Sentio.Enumerations import ChuckSite from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.ProberBase import ProberException from sentio_prober_control.Sentio.CommandGroups.AuxCleaningGroup import AuxCleaningGroup -from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ( - ModuleCommandGroupBase, -) +from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ModuleCommandGroupBase +class ElementInfoResponse: + """ + A class to handle and parse the raw response string for element information + returned from SENTIO's "aux:get_element_info" command. + + Attributes: + raw_response (str): The raw comma-separated response from SENTIO. + element_info (ElementInfo): The parsed element information. + """ + def __init__(self, raw_response: str) -> None: + self.raw_response = raw_response + parts = raw_response.split(",") + if len(parts) < 7: + raise ProberException("Unexpected response for element info.") + # Parse the parts into an ElementInfo object. + self.element_info = ElementInfo( + element_type=ElementType.from_str(parts[0]), + element_subtype=parts[1], + x_position=float(parts[2]), + y_position=float(parts[3]), + spacing=float(parts[4]), + touch_count=int(parts[5]), + life_time=float(parts[6]) + ) + + def get_element_info(self) -> "ElementInfo": + """ + Returns: + The parsed ElementInfo instance. + """ + return self.element_info + +# --- New Enumerator for element types --- +class ElementType(Enum): + """Represents the type of a calibration element.""" + Open = 0 + Short = 1 + Thru = 2 + Load = 3 + Align = 4 + Unknown = 99 + + @classmethod + def from_str(cls, s: str) -> "ElementType": + mapping = { + "open": cls.Open, + "short": cls.Short, + "thru": cls.Thru, + "load" : cls.Load, + "align": cls.Align + } + return mapping.get(s.lower(), cls.Unknown) + +# --- New data class for element information --- +@dataclass +class ElementInfo: + element_type: ElementType + element_subtype: str + x_position: float + y_position: float + spacing: float + touch_count: int + life_time: float + +# --- Updated AuxCommandGroup with eleven API methods --- class AuxCommandGroup(ModuleCommandGroupBase): """This command group contains functions for working with auxiliary sites of the chuck. You are not meant to create instances of this class on your own. Instead, use the aux @@ -21,69 +86,84 @@ def __init__(self, comm) -> None: super().__init__(comm, "aux") self.cleaning: AuxCleaningGroup = AuxCleaningGroup(comm) + # --- Helper method to validate that the given site is an auxiliary site --- + def _validate_aux_site(self, site: "ChuckSite") -> None: + # Only auxiliary sites (e.g. AuxRight, AuxLeft, AuxRight2, AuxLeft2) are allowed. + # Wafer and ChuckCamera are not valid for these commands. + if site in (ChuckSite.Wafer, ChuckSite.ChuckCamera): + raise ProberException(f"Invalid auxiliary site: {site.name}.") + # ------------------------------------------------------------------------- - # 1) retrieve_substrate_data + # 1) retrieve_substrate_data # ------------------------------------------------------------------------- - def retrieve_substrate_data(self, site: Optional[str] = None) -> Tuple[int, List[str]]: + def retrieve_substrate_data(self, site: Optional["ChuckSite"] = None) -> List["ChuckSite"]: """ - Retrieves contact and home information from configuration file and assigns it + Retrieves contact and home information from the configuration file and assigns it to the corresponding auxiliary site. Must be used after system start, since data are not retrieved automatically for safety reasons. Wraps SENTIO's "aux:retrieve_substrate_data" remote command. Args: - site: (Optional) The name/index of the site (e.g. "AuxRight"). + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). If omitted, data for all auxiliary sites is retrieved. Returns: - A tuple of (count, site_list): - - count: The number of sites data has been retrieved for - - site_list: A list of site names for which data was retrieved + A list of ChuckSite for which data was retrieved. """ cmd = "aux:retrieve_substrate_data" if site: - cmd += f" {site}" + self._validate_aux_site(site) + cmd += f" {site.toSentioAbbr()}" self.comm.send(cmd) resp = Response.check_resp(self.comm.read_line()) - - # The response is typically "0,0,,,,..." - # resp.message() returns ",,,..." parts = resp.message().split(",") - if len(parts) == 0: - return 0, [] - - count = int(parts[0]) - site_list = parts[1:] - return count, site_list + if not parts or len(parts) < 1: + return [] + + # The response format is ",,,..." + # Skip the count and convert each token to its corresponding ChuckSite. + sites = [] + for token in parts[1:]: + token = token.strip() + for aux_site in ChuckSite: + if token.lower() == aux_site.toSentioAbbr().lower(): + sites.append(aux_site) + break + return sites # ------------------------------------------------------------------------- - # 2) get_substrate_type + # 2) get_substrate_type # ------------------------------------------------------------------------- - def get_substrate_type(self, site: Optional[str] = None) -> str: + def get_substrate_type(self, site: Optional["ChuckSite"] = None) -> str: """ Retrieves the type of a calibration substrate placed on the chuck. Wraps SENTIO's "aux:get_substrate_type" remote command. Args: - site: (Optional) The site index or name (e.g. "0", "AuxRight"). + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). If omitted, the currently active site is used. Returns: - A string describing the substrate type (e.g. "AC-2", "Wafer", "Brush"). + A string describing the substrate type. + If the remote command returns "OK", an empty string is returned. """ cmd = "aux:get_substrate_type" if site: - cmd += f" {site}" + self._validate_aux_site(site) + cmd += f" {site.toSentioAbbr()}" self.comm.send(cmd) resp = Response.check_resp(self.comm.read_line()) - return resp.message() + substrate_type = resp.message() + if substrate_type.upper() == "OK": + return "" + return substrate_type # ------------------------------------------------------------------------- - # 3) step_to_element + # 3) step_to_element # ------------------------------------------------------------------------- def step_to_element( self, @@ -112,7 +192,7 @@ def step_to_element( Response.check_resp(self.comm.read_line()) # ------------------------------------------------------------------------- - # 4) step_to_dut_element + # 4) step_to_dut_element # ------------------------------------------------------------------------- def step_to_dut_element( self, @@ -132,8 +212,6 @@ def step_to_dut_element( x: (Optional) X position [um]. y: (Optional) Y position [um]. """ - # The typical format is: - # aux:step_to_dut_element ,,, cmd = f"aux:step_to_dut_element {dut_name},{str(move_z).lower()}" if x is not None and y is not None: cmd += f",{x},{y}" @@ -142,13 +220,9 @@ def step_to_dut_element( Response.check_resp(self.comm.read_line()) # ------------------------------------------------------------------------- - # 5) get_element_type + # 5) get_element_type # ------------------------------------------------------------------------- - def get_element_type( - self, - element_standard_id: str, - site: Optional[str] = None - ) -> str: + def get_element_type(self, element_standard_id: str, site: Optional["ChuckSite"] = None) -> ElementType: """ Retrieves the type of an element on a calibration substrate placed on the chuck. @@ -156,63 +230,60 @@ def get_element_type( Args: element_standard_id: The standard ID of the element (e.g. "0102"). - site: (Optional) Substrate type or chuck site (e.g. "AuxRight"). + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). If omitted, the currently active site is used. Returns: - The element type as a string (e.g. "Open", "Short", "Thru", "Align", etc.). + An ElementType enumerator (e.g. ElementType.Short, ElementType.Thru, ElementType.Align, etc). """ cmd = "aux:get_element_type" if site: - cmd += f" {site},{element_standard_id}" + self._validate_aux_site(site) + cmd += f" {site.toSentioAbbr()},{element_standard_id}" else: cmd += f" {element_standard_id}" self.comm.send(cmd) resp = Response.check_resp(self.comm.read_line()) - return resp.message() + result = resp.message() + return ElementType.from_str(result) # ------------------------------------------------------------------------- - # 6) get_substrate_info + # 6) get_substrate_info # ------------------------------------------------------------------------- - def get_substrate_info(self, site: Union[int, str]) -> Dict[str, Union[str, float]]: + def get_substrate_info(self, site: "ChuckSite") -> Tuple[str, str, float]: """ Retrieves information of a calibration substrate or cleaning pad placed on a chuck site. Wraps SENTIO's "aux:get_substrate_info" remote command. Args: - site: The index (0,1,2,...) or name ("AuxRight", "AuxLeft", etc.) of the site. + site: The auxiliary site (e.g. ChuckSite.AuxRight). Returns: - A dict with keys: - "substrate_type" (str), - "substrate_id" (str), - "life_time" (float) in % + A tuple containing: + - substrate_type (str) + - substrate_id (str) [if the id is "OK", an empty string is returned] + - life_time (float) in % """ - cmd = f"aux:get_substrate_info {site}" + self._validate_aux_site(site) + cmd = f"aux:get_substrate_info {site.toSentioAbbr()}" self.comm.send(cmd) resp = Response.check_resp(self.comm.read_line()) - parts = resp.message().split(",") - # Typically: ,, if len(parts) < 3: raise ProberException("Unexpected response for get_substrate_info.") - - return { - "substrate_type": parts[0], - "substrate_id": parts[1], - "life_time": float(parts[2]) - } + substrate_type = parts[0] + substrate_id = parts[1] + if substrate_id.upper() == "OK": + substrate_id = "" + life_time = float(parts[2]) + return (substrate_type, substrate_id, life_time) # ------------------------------------------------------------------------- - # 7) get_element_touch_count + # 7) get_element_touch_count # ------------------------------------------------------------------------- - def get_element_touch_count( - self, - element_standard_id: str, - site: Optional[str] = None - ) -> int: + def get_element_touch_count(self, element_standard_id: str, site: Optional["ChuckSite"] = None) -> int: """ Retrieves the touch count of an element on a calibration substrate placed on the chuck. @@ -220,7 +291,7 @@ def get_element_touch_count( Args: element_standard_id: The standard ID of the element (e.g. "0102"). - site: (Optional) Substrate type or chuck site (e.g. "AuxRight"). + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). If omitted, the currently active site is used. Returns: @@ -228,7 +299,8 @@ def get_element_touch_count( """ cmd = "aux:get_element_touch_count" if site: - cmd += f" {site},{element_standard_id}" + self._validate_aux_site(site) + cmd += f" {site.toSentioAbbr()},{element_standard_id}" else: cmd += f" {element_standard_id}" @@ -237,13 +309,9 @@ def get_element_touch_count( return int(resp.message()) # ------------------------------------------------------------------------- - # 8) get_element_spacing + # 8) get_element_spacing # ------------------------------------------------------------------------- - def get_element_spacing( - self, - element_standard_id: str, - site: Optional[str] = None - ) -> float: + def get_element_spacing(self, element_standard_id: str, site: Optional["ChuckSite"] = None) -> float: """ Retrieves the spacing value of an element on a calibration substrate placed on the chuck. @@ -251,7 +319,7 @@ def get_element_spacing( Args: element_standard_id: The standard ID of the element (e.g. "0102"). - site: (Optional) Substrate type or chuck site (e.g. "AuxRight"). + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). If omitted, the currently active site is used. Returns: @@ -259,7 +327,8 @@ def get_element_spacing( """ cmd = "aux:get_element_spacing" if site: - cmd += f" {site},{element_standard_id}" + self._validate_aux_site(site) + cmd += f" {site.toSentioAbbr()},{element_standard_id}" else: cmd += f" {element_standard_id}" @@ -268,13 +337,9 @@ def get_element_spacing( return float(resp.message()) # ------------------------------------------------------------------------- - # 9) get_element_pos + # 9) get_element_pos # ------------------------------------------------------------------------- - def get_element_pos( - self, - element_standard_id: str, - site: Optional[str] = None - ) -> Tuple[float, float]: + def get_element_pos(self, element_standard_id: str, site: Optional["ChuckSite"] = None) -> Tuple[float, float]: """ Retrieves the (X, Y) position of an element on a calibration substrate placed on the chuck. @@ -282,7 +347,7 @@ def get_element_pos( Args: element_standard_id: The standard ID of the element (e.g. "0102"). - site: (Optional) Substrate type or chuck site (e.g. "AuxRight"). + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). If omitted, the currently active site is used. Returns: @@ -290,7 +355,8 @@ def get_element_pos( """ cmd = "aux:get_element_pos" if site: - cmd += f" {site},{element_standard_id}" + self._validate_aux_site(site) + cmd += f" {site.toSentioAbbr()},{element_standard_id}" else: cmd += f" {element_standard_id}" @@ -299,7 +365,6 @@ def get_element_pos( parts = resp.message().split(",") if len(parts) < 2: raise ProberException("Unexpected response for get_element_pos.") - x = float(parts[0]) y = float(parts[1]) return (x, y) @@ -307,11 +372,7 @@ def get_element_pos( # ------------------------------------------------------------------------- # 10) get_element_life_time # ------------------------------------------------------------------------- - def get_element_life_time( - self, - element_standard_id: str, - site: Optional[str] = None - ) -> float: + def get_element_life_time(self, element_standard_id: str, site: Optional["ChuckSite"] = None) -> float: """ Retrieves the life time (in %) of an element on a calibration substrate placed on the chuck. @@ -319,7 +380,7 @@ def get_element_life_time( Args: element_standard_id: The standard ID of the element (e.g. "0102"). - site: (Optional) Substrate type or chuck site (e.g. "AuxRight"). + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). If omitted, the currently active site is used. Returns: @@ -327,7 +388,8 @@ def get_element_life_time( """ cmd = "aux:get_element_life_time" if site: - cmd += f" {site},{element_standard_id}" + self._validate_aux_site(site) + cmd += f" {site.toSentioAbbr()},{element_standard_id}" else: cmd += f" {element_standard_id}" @@ -338,50 +400,46 @@ def get_element_life_time( # ------------------------------------------------------------------------- # 11) get_element_info # ------------------------------------------------------------------------- - def get_element_info( - self, - element_standard_id: str, - site: Optional[str] = None - ) -> Dict[str, Union[str, float, int]]: + def get_element_info(self, element_standard_id: str, site: Optional["ChuckSite"] = None) -> ElementInfo: """ Retrieves information of a calibration element on a calibration substrate placed on the chuck. Wraps SENTIO's "aux:get_element_info" remote command. + According to the SENTIO specification: + - If a site is given, the command format is: + aux:get_element_info , + - If no site is given, the command format is: + aux:get_element_info + Args: element_standard_id: The standard ID of the element (e.g. "0102"). - site: (Optional) Substrate type or chuck site (e.g. "AuxRight"). - If omitted, the currently active site is used. + site: (Optional) The auxiliary site (e.g. ChuckSite.AuxRight). + If omitted, the currently active site is used. Returns: - A dictionary with keys: - "element_type" (str) -> e.g. "Thru", "Open", "Short", "Align", ... - "element_subtype" (str) -> e.g. "GSG" - "x_position" (float) - "y_position" (float) - "spacing" (float) - "touch_count" (int) - "life_time" (float) -> in % + An ElementInfo object containing: + - element_type (ElementType) + - element_subtype (str) + - x_position (float) + - y_position (float) + - spacing (float) + - touch_count (int) + - life_time (float) """ + # Construct the command based on whether a site is specified cmd = "aux:get_element_info" - if site: - cmd += f" {site},{element_standard_id}" + if site is not None: + self._validate_aux_site(site) + # First parameter is the optional chuck site, second is the element ID + cmd += f" {site.toSentioAbbr()},{element_standard_id}" else: + # Only the element ID is passed if the site is omitted cmd += f" {element_standard_id}" self.comm.send(cmd) resp = Response.check_resp(self.comm.read_line()) - - parts = resp.message().split(",") - if len(parts) < 7: - raise ProberException("Unexpected response for get_element_info.") - - return { - "element_type": parts[0], - "element_subtype": parts[1], - "x_position": float(parts[2]), - "y_position": float(parts[3]), - "spacing": float(parts[4]), - "touch_count": int(parts[5]), - "life_time": float(parts[6]) - } \ No newline at end of file + + # Use the ElementInfoResponse class to parse the raw response + parser = ElementInfoResponse(resp.message()) + return parser.get_element_info() \ No newline at end of file diff --git a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py index 4a98acb..a309ed5 100644 --- a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py @@ -1,6 +1,13 @@ from deprecated import deprecated from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase +from sentio_prober_control.Sentio.ProberBase import ProberException +from typing import List +from enum import Enum + +class DriftType(Enum): + DriftRef = "DriftRef" + Drift = "Drift" class QAlibriaCommandGroup(CommandGroupBase): """ @@ -75,24 +82,26 @@ def set_calibration_drift_probe12(self): # New / Additional Commands # ------------------------------------------------------------------------- - def get_calibration_status(self) -> str: + def check_calibration_status(self) -> None: """ - Retrieve the status of QAlibria. + Checks the calibration status of QAlibria. Wraps SENTIO's "qal:get_calibration_status" remote command. - Returns: - str: The status string of QAlibria (e.g., "OK"). If "OK" and - the system is in Remote mode, calibration is ready. + Raises: + ProberException: If the calibration status is not "OK". """ self.comm.send("qal:get_calibration_status") resp = Response.check_resp(self.comm.read_line()) - return resp.message() + status = resp.message().strip() + if status.upper() != "OK": + raise ProberException(f"Calibration status error: {status}") + def measurement_execute( self, file_name: str, - ports: str = "1,2", + ports: List[int] = [1, 2], correct_by_vna: bool = True, enable_use_ratio: bool = False, enable_switch_term: bool = False @@ -104,14 +113,18 @@ def measurement_execute( Args: file_name: The path to the SNP file. - ports: The port(s) used for the measurement, e.g. "1,2". + ports: A list of port numbers used for the measurement (e.g. [1, 2]). correct_by_vna: Whether to apply VNA correction (True/False). enable_use_ratio: Whether to enable 'Use Ratio b/a' (True/False). enable_switch_term: Whether to enable switch term (True/False). + + Raises: + ProberException: If the remote command returns an error. """ + ports_str = ",".join(str(port) for port in ports) cmd = ( f"qal:measurement_execute " - f"{file_name},{ports}," + f"{file_name},{ports_str}," f"{str(correct_by_vna).lower()}," f"{str(enable_use_ratio).lower()}," f"{str(enable_switch_term).lower()}" @@ -128,20 +141,25 @@ def reset_ets(self) -> None: self.comm.send("qal:reset_ets") Response.check_resp(self.comm.read_line()) - def set_ets(self, ports: str, path: str) -> None: + def set_ets(self, port: int, path: str, ets_mode: int = 0) -> None: """ Set error terms in the buffer. Wraps SENTIO's "qal:set_ets" remote command. Args: - ports: Port(s) of error terms (e.g. "12"). + port: Port of error terms (e.g. 12). path: Path to the error terms file in the buffer (e.g. "D:\\temp\\ets.txt"). + ets_mode: An integer parameter as defined by the remote command spec (default is 0). + + Raises: + ProberException: If the remote command returns an error. """ - cmd = f"qal:set_ets {ports},{path}" + cmd = f"qal:set_ets {port},{path},{ets_mode}" self.comm.send(cmd) Response.check_resp(self.comm.read_line()) + def send_ets_to_vna(self, cal_set_name: str) -> None: """ Send current error terms in QAlibria buffer to the VNA. @@ -155,7 +173,7 @@ def send_ets_to_vna(self, cal_set_name: str) -> None: self.comm.send(cmd) Response.check_resp(self.comm.read_line()) - def clear_dut_network(self, dut_name: str, drift_type: str, update_ui: bool) -> None: + def clear_dut_network(self, dut_name: str, drift_type: DriftType, update_ui: bool) -> None: """ Clear network data for a DUT. @@ -163,16 +181,17 @@ def clear_dut_network(self, dut_name: str, drift_type: str, update_ui: bool) -> Args: dut_name: The name of the DUT (e.g. "RefDUT"). - drift_type: The type of drift data to clear ("DriftRef" or "Drift"). + drift_type: The type of drift data to clear (DriftType.DriftRef or DriftType.Drift). update_ui: Whether to update the UI (True/False). + + Raises: + ProberException: If the remote command returns an error. """ - cmd = ( - f"qal:clear_dut_network " - f"{dut_name},{drift_type},{str(update_ui).lower()}" - ) + cmd = f"qal:clear_dut_network {dut_name},{drift_type.value},{str(update_ui).lower()}" self.comm.send(cmd) Response.check_resp(self.comm.read_line()) + def vna_write(self, vna_command: str) -> None: """ Write a remote command to the VNA. diff --git a/sentio_prober_control/UnitTest/TestAuxCommandGroup.py b/sentio_prober_control/UnitTest/TestAuxCommandGroup.py index dd7e040..459c733 100644 --- a/sentio_prober_control/UnitTest/TestAuxCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestAuxCommandGroup.py @@ -3,13 +3,22 @@ from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp from sentio_prober_control.Sentio.ProberSentio import SentioProber from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.Enumerations import ChuckSite +from sentio_prober_control.Sentio.ProberBase import ProberException +# Import our AuxCommandGroup and related types (without SubstrateType) +from sentio_prober_control.Sentio.CommandGroups.AuxCommandGroup import ( + AuxCommandGroup, + ElementInfo, + ElementType, + ElementInfoResponse, +) # A FakeResponse class that strips the "0,0," prefix from the response. class FakeResponse: - def __init__(self, message): + def __init__(self, message: str): self._message = message - def message(self): + def message(self) -> str: prefix = "0,0," if self._message.startswith(prefix): return self._message[len(prefix):] @@ -26,7 +35,7 @@ def setUp(self): self.mock_comm = MagicMock(spec=CommunicatorTcpIp) # Create a SentioProber instance (which instantiates the aux command group). self.prober = SentioProber(self.mock_comm) - self.aux = self.prober.aux + self.aux: AuxCommandGroup = self.prober.aux def tearDown(self): # Restore the original Response.check_resp. @@ -34,18 +43,20 @@ def tearDown(self): # 1) Test retrieve_substrate_data def test_retrieve_substrate_data_no_site(self): - self.mock_comm.read_line.return_value = "0,0,3,Aux1,Aux2,Aux3" - count, sites = self.aux.retrieve_substrate_data() + # Return response with three sites: AuxRight, AuxLeft, AuxRight2 + self.mock_comm.read_line.return_value = "0,0,3,AuxRight,AuxLeft,AuxRight2" + sites = self.aux.retrieve_substrate_data() self.mock_comm.send.assert_called_with("aux:retrieve_substrate_data") - self.assertEqual(count, 3) - self.assertEqual(sites, ["Aux1", "Aux2", "Aux3"]) + expected_sites = [ChuckSite.AuxRight, ChuckSite.AuxLeft, ChuckSite.AuxRight2] + self.assertEqual(sites, expected_sites) def test_retrieve_substrate_data_with_site(self): - self.mock_comm.read_line.return_value = "0,0,1,Aux1" - count, sites = self.aux.retrieve_substrate_data("AuxRight") - self.mock_comm.send.assert_called_with("aux:retrieve_substrate_data AuxRight") - self.assertEqual(count, 1) - self.assertEqual(sites, ["Aux1"]) + self.mock_comm.read_line.return_value = "0,0,1,AuxRight" + sites = self.aux.retrieve_substrate_data(ChuckSite.AuxRight) + expected_command = "aux:retrieve_substrate_data " + ChuckSite.AuxRight.toSentioAbbr() + self.mock_comm.send.assert_called_with(expected_command) + expected_sites = [ChuckSite.AuxRight] + self.assertEqual(sites, expected_sites) # 2) Test get_substrate_type def test_get_substrate_type_no_site(self): @@ -56,8 +67,9 @@ def test_get_substrate_type_no_site(self): def test_get_substrate_type_with_site(self): self.mock_comm.read_line.return_value = "0,0,AC-2" - result = self.aux.get_substrate_type("0") - self.mock_comm.send.assert_called_with("aux:get_substrate_type 0") + result = self.aux.get_substrate_type(ChuckSite.AuxLeft) + expected_command = "aux:get_substrate_type " + ChuckSite.AuxLeft.toSentioAbbr() + self.mock_comm.send.assert_called_with(expected_command) self.assertEqual(result, "AC-2") # 3) Test step_to_element @@ -86,22 +98,31 @@ def test_get_element_type_without_site(self): result = self.aux.get_element_type("0102") expected_command = "aux:get_element_type 0102" self.mock_comm.send.assert_called_with(expected_command) - self.assertEqual(result, "Short") + self.assertEqual(result, ElementType.Short) def test_get_element_type_with_site(self): self.mock_comm.read_line.return_value = "0,0,Thru" - result = self.aux.get_element_type("0102", "AuxRight") - expected_command = "aux:get_element_type AuxRight,0102" + result = self.aux.get_element_type("0102", ChuckSite.AuxRight) + expected_command = "aux:get_element_type " + ChuckSite.AuxRight.toSentioAbbr() + ",0102" self.mock_comm.send.assert_called_with(expected_command) - self.assertEqual(result, "Thru") + self.assertEqual(result, ElementType.Thru) # 6) Test get_substrate_info def test_get_substrate_info(self): self.mock_comm.read_line.return_value = "0,0,AC-2,ac2,4.48" - result = self.aux.get_substrate_info("AuxLeft") - expected_command = "aux:get_substrate_info AuxLeft" + result = self.aux.get_substrate_info(ChuckSite.AuxLeft) + expected_command = "aux:get_substrate_info " + ChuckSite.AuxLeft.toSentioAbbr() self.mock_comm.send.assert_called_with(expected_command) - self.assertEqual(result, {"substrate_type": "AC-2", "substrate_id": "ac2", "life_time": 4.48}) + expected = ("AC-2", "ac2", 4.48) + self.assertEqual(result, expected) + + def test_get_substrate_info_invalid_site_wafer(self): + with self.assertRaises(ProberException): + self.aux.get_substrate_info(ChuckSite.Wafer) + + def test_get_substrate_info_invalid_site_chuckcamera(self): + with self.assertRaises(ProberException): + self.aux.get_substrate_info(ChuckSite.ChuckCamera) # 7) Test get_element_touch_count def test_get_element_touch_count_without_site(self): @@ -113,8 +134,8 @@ def test_get_element_touch_count_without_site(self): def test_get_element_touch_count_with_site(self): self.mock_comm.read_line.return_value = "0,0,7" - result = self.aux.get_element_touch_count("0102", "AuxRight") - expected_command = "aux:get_element_touch_count AuxRight,0102" + result = self.aux.get_element_touch_count("0102", ChuckSite.AuxRight) + expected_command = "aux:get_element_touch_count " + ChuckSite.AuxRight.toSentioAbbr() + ",0102" self.mock_comm.send.assert_called_with(expected_command) self.assertEqual(result, 7) @@ -128,8 +149,8 @@ def test_get_element_spacing_without_site(self): def test_get_element_spacing_with_site(self): self.mock_comm.read_line.return_value = "0,0,200.5" - result = self.aux.get_element_spacing("0102", "AuxLeft") - expected_command = "aux:get_element_spacing AuxLeft,0102" + result = self.aux.get_element_spacing("0102", ChuckSite.AuxLeft) + expected_command = "aux:get_element_spacing " + ChuckSite.AuxLeft.toSentioAbbr() + ",0102" self.mock_comm.send.assert_called_with(expected_command) self.assertEqual(result, 200.5) @@ -143,8 +164,8 @@ def test_get_element_pos_without_site(self): def test_get_element_pos_with_site(self): self.mock_comm.read_line.return_value = "0,0,100.0,200.0" - result = self.aux.get_element_pos("0102", "AuxRight") - expected_command = "aux:get_element_pos AuxRight,0102" + result = self.aux.get_element_pos("0102", ChuckSite.AuxRight) + expected_command = "aux:get_element_pos " + ChuckSite.AuxRight.toSentioAbbr() + ",0102" self.mock_comm.send.assert_called_with(expected_command) self.assertEqual(result, (100.0, 200.0)) @@ -158,43 +179,44 @@ def test_get_element_life_time_without_site(self): def test_get_element_life_time_with_site(self): self.mock_comm.read_line.return_value = "0,0,85.5" - result = self.aux.get_element_life_time("0102", "AuxLeft") - expected_command = "aux:get_element_life_time AuxLeft,0102" + result = self.aux.get_element_life_time("0102", ChuckSite.AuxLeft) + expected_command = "aux:get_element_life_time " + ChuckSite.AuxLeft.toSentioAbbr() + ",0102" self.mock_comm.send.assert_called_with(expected_command) self.assertEqual(result, 85.5) # 11) Test get_element_info def test_get_element_info_without_site(self): + # Raw response: element_type,element_subtype,x_position,y_position,spacing,touch_count,life_time self.mock_comm.read_line.return_value = "0,0,Thru,GSG,150.0,250.0,50.0,2,95.0" result = self.aux.get_element_info("0102") expected_command = "aux:get_element_info 0102" self.mock_comm.send.assert_called_with(expected_command) - expected_dict = { - "element_type": "Thru", - "element_subtype": "GSG", - "x_position": 150.0, - "y_position": 250.0, - "spacing": 50.0, - "touch_count": 2, - "life_time": 95.0 - } - self.assertEqual(result, expected_dict) + expected = ElementInfo( + element_type=ElementType.Thru, + element_subtype="GSG", + x_position=150.0, + y_position=250.0, + spacing=50.0, + touch_count=2, + life_time=95.0 + ) + self.assertEqual(result, expected) def test_get_element_info_with_site(self): self.mock_comm.read_line.return_value = "0,0,Open,NonGSG,100.0,200.0,30.0,1,99.0" - result = self.aux.get_element_info("0102", "AuxRight") - expected_command = "aux:get_element_info AuxRight,0102" - self.mock_comm.send.assert_called_with(expected_command) - expected_dict = { - "element_type": "Open", - "element_subtype": "NonGSG", - "x_position": 100.0, - "y_position": 200.0, - "spacing": 30.0, - "touch_count": 1, - "life_time": 99.0 - } - self.assertEqual(result, expected_dict) + result = self.aux.get_element_info("0102", ChuckSite.AuxRight) + expected_command = "aux:get_element_info " + ChuckSite.AuxRight.toSentioAbbr() + ",0102" + self.mock_comm.send.assert_called_with(expected_command) + expected = ElementInfo( + element_type=ElementType.Open, + element_subtype="NonGSG", + x_position=100.0, + y_position=200.0, + spacing=30.0, + touch_count=1, + life_time=99.0 + ) + self.assertEqual(result, expected) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestQAlibriaCommandGroup.py b/sentio_prober_control/UnitTest/TestQAlibriaCommandGroup.py index b0f9aeb..bc3162a 100644 --- a/sentio_prober_control/UnitTest/TestQAlibriaCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestQAlibriaCommandGroup.py @@ -1,8 +1,9 @@ import unittest from unittest.mock import MagicMock, call from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp -from sentio_prober_control.Sentio.CommandGroups.QAlibriaCommandGroup import QAlibriaCommandGroup +from sentio_prober_control.Sentio.CommandGroups.QAlibriaCommandGroup import QAlibriaCommandGroup, DriftType from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.ProberBase import ProberException # Dummy response class to simulate Response objects from check_resp. class DummyResponse: @@ -70,15 +71,22 @@ def test_set_calibration_drift_probe12_deprecated(self): self.mock_comm.send.assert_has_calls(expected_calls) self.assertEqual(result, "SecondResponse") - def test_get_calibration_status(self): + def test_check_calibration_status_ok(self): self.mock_comm.read_line.return_value = "0,0,OK" - status = self.qal.get_calibration_status() + # check_calibration_status should not return any value and not raise an exception. + result = self.qal.check_calibration_status() self.mock_comm.send.assert_called_with("qal:get_calibration_status") - self.assertEqual(status, "OK") + self.assertIsNone(result) + + def test_check_calibration_status_error(self): + self.mock_comm.read_line.return_value = "0,0,ERROR" + with self.assertRaises(ProberException): + self.qal.check_calibration_status() def test_measurement_execute(self): self.mock_comm.read_line.return_value = "0,0,OK" - self.qal.measurement_execute("test.snp", "1,2", True, False, True) + # Now use ports as a list of integers. + self.qal.measurement_execute("test.snp", [1, 2], True, False, True) expected_cmd = "qal:measurement_execute test.snp,1,2,true,false,true" self.mock_comm.send.assert_called_with(expected_cmd) @@ -89,8 +97,9 @@ def test_reset_ets(self): def test_set_ets(self): self.mock_comm.read_line.return_value = "0,0,OK" - self.qal.set_ets("12", "D:\\temp\\ets.txt") - expected_cmd = "qal:set_ets 12,D:\\temp\\ets.txt" + # Pass an integer for port. + self.qal.set_ets(12, "D:\\temp\\ets.txt") + expected_cmd = "qal:set_ets 12,D:\\temp\\ets.txt,0" self.mock_comm.send.assert_called_with(expected_cmd) def test_send_ets_to_vna(self): @@ -101,7 +110,8 @@ def test_send_ets_to_vna(self): def test_clear_dut_network(self): self.mock_comm.read_line.return_value = "0,0,OK" - self.qal.clear_dut_network("RefDUT", "DriftRef", True) + # Pass DriftType.DriftRef instead of a string. + self.qal.clear_dut_network("RefDUT", DriftType.DriftRef, True) expected_cmd = "qal:clear_dut_network RefDUT,DriftRef,true" self.mock_comm.send.assert_called_with(expected_cmd) @@ -125,4 +135,4 @@ def test_vna_read(self): self.assertEqual(result, "Data") if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 9dbf0d887b86fb5e9f09ff23c8e858495f2292e2 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Tue, 8 Apr 2025 09:59:09 +0800 Subject: [PATCH 09/41] Jeff add chuck and project handling commands --- sentio_prober_control/Sentio/Enumerations.py | 109 ++++++ sentio_prober_control/Sentio/ProberSentio.py | 365 +++++++++++++++++- .../UnitTest/TestProberSentio.py | 241 ++++++++++++ 3 files changed, 713 insertions(+), 2 deletions(-) diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index abcd789..d84712e 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -255,6 +255,20 @@ class ChuckPositionHint(Enum): Center = 0 FrontLoad = 1 SideLoad = 2 + OffAxisCamera = 3 + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Probing": ChuckPositionHint.Center, + "FrontLoad": ChuckPositionHint.FrontLoad, + "SideLoad": ChuckPositionHint.SideLoad, + "OffAxisCamera": ChuckPositionHint.OffAxisCamera + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckPositionHint abbreviation: {abbr}") class ChuckSite(Enum): @@ -270,6 +284,8 @@ class ChuckSite(Enum): AuxRight2 (3): Secondary right auxilliary site (if available) AuxLeft2 (4): Secondary left auxilliary site (if available) ChuckCamera (5): The chuck camera + SiPhSetHoverHeight (6): Siph set hover height site + SiPhFiberPowerMeasure (7): Siph fiber power measure site """ Wafer = 0 @@ -278,6 +294,8 @@ class ChuckSite(Enum): AuxRight2 = 3 AuxLeft2 = 4 ChuckCamera = 5 + SiPhSetHoverHeight = 6 + SiPhFiberPowerMeasure = 7 def toSentioAbbr(self): switcher = { @@ -287,9 +305,28 @@ def toSentioAbbr(self): ChuckSite.AuxRight: "AuxRight", ChuckSite.AuxRight2: "AuxRight2", ChuckSite.ChuckCamera: "ChuckCamera", + ChuckSite.SiPhSetHoverHeight: "SiPhSetHoverHeight", + ChuckSite.SiPhFiberPowerMeasure: "SiPhFiberPowerMeasure", } return switcher.get(self, "Invalid chuck site") + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Wafer": ChuckSite.Wafer, + "AuxRight": ChuckSite.AuxRight, + "AuxLeft": ChuckSite.AuxLeft, + "AuxRight2": ChuckSite.AuxRight2, + "AuxLeft2": ChuckSite.AuxLeft2, + "ChuckCamera": ChuckSite.ChuckCamera, + "SiPhSetHoverHeight": ChuckSite.SiPhSetHoverHeight, + "SiPhFiberPowerMeasure": ChuckSite.SiPhFiberPowerMeasure, + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckSite abbreviation: {abbr}") + class ChuckThetaReference(Enum): """Reference to use for chuck theat motions. @@ -1761,3 +1798,75 @@ def toSentioAbbr(self): HighPurgeState.Off: "OFF", } return switcher.get(self, "Invalid HighPurgeState") + +class ChuckSpeed(Enum): + Fast = 0 + Normal = 1 + Slow = 2 + Jog = 3 + Index = 4 + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Fast": ChuckSpeed.Fast, + "Normal": ChuckSpeed.Normal, + "Slow": ChuckSpeed.Slow, + "Jog": ChuckSpeed.Jog, + "Index": ChuckSpeed.Index, + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckSpeed abbreviation: {abbr}") + +class VacuumState(Enum): + Off = 0 + On = 1 + + def toSentioAbbr(self) -> str: + return { + VacuumState.Off: "Off", + VacuumState.On: "On", + }.get(self, "Invalid VacuumState") + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "0": VacuumState.Off, + "1": VacuumState.On + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown VacuumState abbreviation: {abbr}") + +class HighPowerAirState(Enum): + Off = 0 + On = 1 + + def toSentioAbbr(self) -> str: + return { + HighPowerAirState.Off: "0", + HighPowerAirState.On: "1", + }.get(self, "Invalid HighPowerAirState") + +class SoftContactState(Enum): + Disable = 0 + Enable = 1 + + def toSentioAbbr(self) -> str: + return { + SoftContactState.Disable: "0", + SoftContactState.Enable: "1", + }.get(self, "Invalid SoftContactState") + +class UserCoordState(Enum): + Chuck = 0 + Scope = 1 + + def toSentioAbbr(self) -> str: + return { + UserCoordState.Chuck: "chuck", + UserCoordState.Scope: "scope" + }.get(self, "Invalid UserCoordState") \ No newline at end of file diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index fc914ff..8904da9 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -19,7 +19,13 @@ Stage, SteppingContactMode, VceZReference, - WorkArea + WorkArea, + ChuckPositionHint, + ChuckSpeed, + VacuumState, + HighPowerAirState, + SoftContactState, + UserCoordState ) from sentio_prober_control.Sentio.ProberBase import ProberBase, ProberException @@ -1144,4 +1150,359 @@ def step_scope_site_next(self) -> tuple[str, float, float]: offset_x = float(tok[1]) offset_y = float(tok[2]) - return site_id, offset_x, offset_y \ No newline at end of file + return site_id, offset_x, offset_y + + def get_chuck_position_hint(self) -> tuple[ChuckPositionHint, ChuckSite]: + """Get a verbal representation of the current chuck position and the active site. + + Returns: + position_hint (str): Describes the chuck's general position, e.g., "Probing", "FrontLoad", "SideLoad". + site (str): The current chuck site, e.g., "Wafer", "AuxRight", "ChuckCameraLR", etc. + """ + self.comm.send("get_chuck_position_hint") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + pos_hint_str = tok[0] + site_str = tok[1] + + pos_hint = ChuckPositionHint.fromSentioAbbr(pos_hint_str) + site = ChuckSite.fromSentioAbbr(site_str) + + return pos_hint, site + + def get_chuck_site_count(self) -> int: + """Retrieve the number of chuck sites. + + Wraps SENTIO's "get_chuck_site_count" remote command. + + Returns: + count (int): The number of chuck sites. + """ + self.comm.send("get_chuck_site_count") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + count = int(tok[0]) + + return count + + def get_chuck_site_index(self, name: str | None = None) -> int: + """Retrieve the index of a chuck site by name. If no name is provided, returns the index of the current site. + + Args: + name (str, optional): The name of the chuck site. If None, the current site index is returned. + + Returns: + index (int): The index of the chuck site. + """ + if name is None: + self.comm.send("get_chuck_site_index") + else: + self.comm.send(f"get_chuck_site_index {name}") + + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + index = int(tok[0]) + + return index + + def get_chuck_site_name(self, index: int | None = None) -> ChuckSite: + """Retrieve the name of a chuck site. If no index is given, returns the current site's name. + + Args: + index (int, optional): The index of the chuck site. If None, gets the name of the current site. + + Returns: + name (str): The name of the chuck site. + """ + if index is None: + self.comm.send("get_chuck_site_name") + else: + self.comm.send(f"get_chuck_site_name {index}") + + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_str = tok[0] + site = ChuckSite.fromSentioAbbr(site_str) + + return site + + def get_chuck_site_pos(self, site: ChuckSite | None = None) -> tuple[float, float, float]: + """Retrieve position information of a chuck site. + + Args: + site (ChuckSite, optional): The chuck site to query. If None, uses the current active site. + + Returns: + tuple[float, float, float]: (home_x, home_y, angle) in µm and degrees. + """ + if site is not None: + site_arg = site.toSentioAbbr() + self.comm.send(f"get_chuck_site_pos {site_arg}") + else: + self.comm.send("get_chuck_site_pos") + + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + home_x = float(tok[0]) + home_y = float(tok[1]) + angle = float(tok[2]) + return home_x, home_y, angle + + def get_chuck_speed(self) -> ChuckSpeed: + """Retrieve the current chuck speed setting. + + Returns: + ChuckSpeed: Enum representing current chuck speed. + """ + self.comm.send("get_chuck_speed") + resp = Response.check_resp(self.comm.read_line()) + speed_str = resp.message().split(",")[0] + + speed = ChuckSpeed.fromSentioAbbr(speed_str) + + return speed + + from sentio_prober_control.Sentio.Enumerations import ChuckSite + + def get_vacuum_status(self, site: ChuckSite | None = None) -> VacuumState: + """Get the vacuum status of a chuck site. + + Args: + site (ChuckSite, optional): The chuck site to query. If None, queries the currently active site. + + Returns: + VacuumState: Enum indicating On or Off. + """ + if site is not None: + site_arg = site.toSentioAbbr() + self.comm.send(f"get_vacuum_status {site_arg}") + else: + self.comm.send("get_vacuum_status") + + resp = Response.check_resp(self.comm.read_line()) + status = resp.message().split(",")[0] + + return VacuumState.fromSentioAbbr(status) + + def move_chuck_hover(self) -> float: + """Move the chuck to hover height. + + If hover height is not enabled, the move will not be carried out. + + Returns: + z (float): The new Z position in micrometers with respect to zero. + """ + self.comm.send("move_chuck_hover") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + z_pos = float(tok[0]) + + return z_pos + + def move_chuck_index(self, ref: str, x_steps: int, y_steps: int) -> tuple[float, float]: + """Move chuck xy by a number of index steps relative to a reference point. + + If the chuck is above separation height, it will move to separation before and return after movement. + + Args: + ref (str): The reference point ("Zero", "Home", "Relative", "Center"). Case-insensitive. + x_steps (int): Number of steps in X direction. + y_steps (int): Number of steps in Y direction. + + Returns: + new_x (float): New X position (in µm, from axis zero). + new_y (float): New Y position (in µm, from axis zero). + """ + self.comm.send(f"move_chuck_index {ref}, {x_steps}, {y_steps}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + new_x = float(tok[0]) + new_y = float(tok[1]) + + return new_x, new_y + + def move_chuck_xyt(self, x_offset: float, y_offset: float, theta_offset: float) -> None: + """Move chuck xy and theta to match pattern to field of view center. + + If chuck is above separation height, it will move to separation first and stay there after movement. + + Args: + x_offset (float): Offset in X direction (in µm). + y_offset (float): Offset in Y direction (in µm). + theta_offset (float): Offset in theta (in degrees). + + Returns: + result (str): Usually returns "OK" on success. + """ + self.comm.send(f"move_chuck_xyt {x_offset}, {y_offset}, {theta_offset}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_overtravel_gap(self, gap: float) -> None: + """Set overtravel gap for all chuck sites. + + Args: + gap (float): The overtravel gap in micrometers. + + Returns: + result (str): Typically "OK" if successfully set. + """ + self.comm.send(f"set_chuck_overtravel_gap {gap}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_separation_gap(self, gap: float) -> None: + """Set separation gap for all chuck sites. + + Args: + gap (float): The separation gap in micrometers. + + Returns: + result (str): Typically "OK" if successfully set. + """ + self.comm.send(f"set_chuck_separation_gap {gap}") + Response.check_resp(self.comm.read_line()) + + from sentio_prober_control.Sentio.Enumerations import ChuckSite + from sentio_prober_control.Sentio.Response import Response + + def set_chuck_site_overtravel_gap(self, site: ChuckSite, gap: float) -> None: + """Set overtravel gap for a specific chuck site. + + Args: + site (ChuckSite): The chuck site to apply the overtravel gap to. Cannot be None. + gap (float): Overtravel gap in micrometers. + + Raises: + ValueError: If site is None. + """ + if site is None: + raise ValueError("site is required and cannot be None") + + site_arg = site.toSentioAbbr() + self.comm.send(f"set_chuck_site_overtravel_gap {site_arg}, {gap}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_site_pos( + self, + x: float | None = None, + y: float | None = None, + theta: float | None = None, + site: ChuckSite | None = None + ) -> None: + """Set home position information of a chuck site. + + Args: + x (float, optional): Home X position in µm. + y (float, optional): Home Y position in µm. + theta (float, optional): Home angle in degrees. + site (ChuckSite, optional): Site enum. If None, uses current chuck site. + + Returns: + None + """ + if x is not None and y is not None and theta is not None: + site_arg = site.toSentioAbbr() if site is not None else None + + if site_arg: + self.comm.send(f"set_chuck_site_pos {site_arg},{x},{y},{theta}") + else: + self.comm.send(f"set_chuck_site_pos {x},{y},{theta}") + else: + self.comm.send("set_chuck_site_pos") + + Response.check_resp(self.comm.read_line()) + + def set_chuck_site_separation_gap(self, site: ChuckSite, gap: float) -> None: + """Set separation gap for a specific chuck site. + + Args: + site (ChuckSite): The chuck site to apply the gap to. This parameter is required. + gap (float): Separation gap in micrometers. + + Raises: + ValueError: If site is None. + """ + if site is None: + raise ValueError("site is required and cannot be None") + + site_arg = site.toSentioAbbr() + self.comm.send(f"set_chuck_site_separation_gap {site_arg}, {gap}") + Response.check_resp(self.comm.read_line()) + + def set_high_power_air(self, state: HighPowerAirState) -> None: + """Switch the air of high power prober card on or off. + + Args: + state (HighPowerAirState): Enum value indicating whether to enable or disable air. + + Raises: + ValueError: If state is None or not a valid enum. + """ + if state is None: + raise ValueError("state is required and must be a valid HighPowerAirState enum") + + self.comm.send(f"set_high_power_air {state.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_soft_contact(self, state: SoftContactState) -> None: + """Enable or disable soft contact mode. + + Args: + state (SoftContactState): Enum indicating whether to enable or disable soft contact. + + Raises: + ValueError: If state is None. + """ + if state is None: + raise ValueError("state is required and must be a valid SoftContactState enum") + + self.comm.send(f"set_soft_contact {state.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_user_coordinate_origin(self, state: UserCoordState, x: float, y: float) -> None: + """Set the new user coordinate origin (X, Y) for chuck or scope. + + Args: + state (UserCoordState): Either UserCoordState.Chuck or UserCoordState.Scope. + x (float): X offset in micrometers. + y (float): Y offset in micrometers. + + Raises: + ValueError: If any parameter is invalid. + """ + if state is None: + raise ValueError("state is required and must be a valid UserCoordState") + + cmd = f"set_user_coordinate_origin {state.toSentioAbbr()},{x},{y}" + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + def create_project(self, path_or_name: str) -> str: + """Create a new project. Overwrites if a project with the same name exists. + + Args: + path_or_name (str): Full path or project name. + If only a name is given, it will be created in the default project directory. + + Returns: + full_path (str): The full path to the created project file (*.trex). + """ + self.comm.send(f"create_project {path_or_name}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + full_path = tok[0] + + return full_path diff --git a/sentio_prober_control/UnitTest/TestProberSentio.py b/sentio_prober_control/UnitTest/TestProberSentio.py index 3a81988..fa75d64 100644 --- a/sentio_prober_control/UnitTest/TestProberSentio.py +++ b/sentio_prober_control/UnitTest/TestProberSentio.py @@ -4,6 +4,16 @@ from sentio_prober_control.Sentio.ProberSentio import SentioProber from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.Enumerations import ( + ChuckPositionHint, + ChuckSite, + ChuckSpeed, + VacuumState, + HighPowerAirState, + SoftContactState, + UserCoordState +) + class TestScopeCommandGroup(unittest.TestCase): def setUp(self): """Initialize the mock communicator and ScopeCommandGroup instance.""" @@ -61,6 +71,237 @@ def test_step_scope_site_next(self): self.assertEqual(offset_x, 1000.0) self.assertEqual(offset_y, 1000.0) + def test_get_chuck_position_hint(self): + """Test get_chuck_position_hint returns correct enums.""" + self.mock_comm.read_line.return_value = "0,0,Probing,Wafer" + position, site = self.test_prober.get_chuck_position_hint() + self.mock_comm.send.assert_called_with("get_chuck_position_hint") + self.assertEqual(position, ChuckPositionHint.Center) + self.assertEqual(site, ChuckSite.Wafer) + + def test_get_chuck_site_count(self): + """Test get_chuck_site_count returns correct integer.""" + self.mock_comm.read_line.return_value = "0,0,5" + count = self.test_prober.get_chuck_site_count() + self.mock_comm.send.assert_called_with("get_chuck_site_count") + self.assertEqual(count, 5) + + def test_get_chuck_site_index_without_name(self): + """Test get_chuck_site_index without argument returns correct index.""" + self.mock_comm.read_line.return_value = "0,0,2" + index = self.test_prober.get_chuck_site_index() + self.mock_comm.send.assert_called_with("get_chuck_site_index") + self.assertEqual(index, 2) + + def test_get_chuck_site_index_with_name(self): + """Test get_chuck_site_index with a site name returns correct index.""" + self.mock_comm.read_line.return_value = "0,0,3" + index = self.test_prober.get_chuck_site_index("Wafer") + self.mock_comm.send.assert_called_with("get_chuck_site_index Wafer") + self.assertEqual(index, 3) + + def test_get_chuck_site_name_without_index(self): + """Test get_chuck_site_name with no index.""" + self.mock_comm.read_line.return_value = "0,0,Wafer" + result = self.test_prober.get_chuck_site_name() + self.mock_comm.send.assert_called_with("get_chuck_site_name") + self.assertEqual(result, ChuckSite.Wafer) + + def test_get_chuck_site_name_with_index(self): + """Test get_chuck_site_name with specific index.""" + self.mock_comm.read_line.return_value = "0,0,AuxLeft" + result = self.test_prober.get_chuck_site_name(2) + self.mock_comm.send.assert_called_with("get_chuck_site_name 2") + self.assertEqual(result, ChuckSite.AuxLeft) + + def test_get_chuck_site_pos_with_site(self): + """Test get_chuck_site_pos with ChuckSite enum.""" + self.mock_comm.read_line.return_value = "0,0,25000,40000,-45" + x, y, theta = self.test_prober.get_chuck_site_pos(ChuckSite.Wafer) + self.mock_comm.send.assert_called_with("get_chuck_site_pos Wafer") + self.assertEqual(x, 25000.0) + self.assertEqual(y, 40000.0) + self.assertEqual(theta, -45.0) + + def test_get_chuck_site_pos_without_site(self): + """Test get_chuck_site_pos without site (use current active site).""" + self.mock_comm.read_line.return_value = "0,0,1000,2000,15" + x, y, theta = self.test_prober.get_chuck_site_pos() + self.mock_comm.send.assert_called_with("get_chuck_site_pos") + self.assertEqual(x, 1000.0) + self.assertEqual(y, 2000.0) + self.assertEqual(theta, 15.0) + + def test_get_chuck_speed(self): + """Test get_chuck_speed returns correct ChuckSpeed enum.""" + self.mock_comm.read_line.return_value = "0,0,Fast" + result = self.test_prober.get_chuck_speed() + self.mock_comm.send.assert_called_with("get_chuck_speed") + self.assertEqual(result, ChuckSpeed.Fast) + + def test_get_vacuum_status_on_with_site(self): + """Test get_vacuum_status returns ON with ChuckSite specified.""" + self.mock_comm.read_line.return_value = "0,0,1" + result = self.test_prober.get_vacuum_status(ChuckSite.AuxLeft) + self.mock_comm.send.assert_called_with("get_vacuum_status AuxLeft") + self.assertEqual(result, VacuumState.On) + + def test_get_vacuum_status_off_with_site(self): + """Test get_vacuum_status returns OFF with ChuckSite specified.""" + self.mock_comm.read_line.return_value = "0,0,0" + result = self.test_prober.get_vacuum_status(ChuckSite.Wafer) + self.mock_comm.send.assert_called_with("get_vacuum_status Wafer") + self.assertEqual(result, VacuumState.Off) + + def test_get_vacuum_status_without_site(self): + """Test get_vacuum_status returns ON using current active site.""" + self.mock_comm.read_line.return_value = "0,0,1" + result = self.test_prober.get_vacuum_status() + self.mock_comm.send.assert_called_with("get_vacuum_status") + self.assertEqual(result, VacuumState.On) + + def test_move_chuck_hover(self): + """Test move_chuck_hover returns correct Z position.""" + self.mock_comm.read_line.return_value = "0,0,16000.0" + z = self.test_prober.move_chuck_hover() + self.mock_comm.send.assert_called_with("move_chuck_hover") + self.assertEqual(z, 16000.0) + + def test_move_chuck_index(self): + """Test move_chuck_index returns correct new XY position.""" + self.mock_comm.read_line.return_value = "0,0,1000.0,3000.0" + x, y = self.test_prober.move_chuck_index("Home", 5, -3) + self.mock_comm.send.assert_called_with("move_chuck_index Home, 5, -3") + self.assertEqual(x, 1000.0) + self.assertEqual(y, 3000.0) + + def test_move_chuck_xyt(self): + """Test move_chuck_xyt sends correct command and completes without error.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.move_chuck_xyt(5.0, -10.0, 0.5) + self.mock_comm.send.assert_called_with("move_chuck_xyt 5.0, -10.0, 0.5") + self.assertIsNone(result) + + def test_set_chuck_overtravel_gap(self): + """Test set_chuck_overtravel_gap sends correct command and succeeds.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_overtravel_gap(50.0) + self.mock_comm.send.assert_called_with("set_chuck_overtravel_gap 50.0") + self.assertIsNone(result) + + def test_set_chuck_separation_gap(self): + """Test set_chuck_separation_gap sends correct command and succeeds.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_separation_gap(250.0) + self.mock_comm.send.assert_called_with("set_chuck_separation_gap 250.0") + self.assertIsNone(result) + + def test_set_chuck_site_overtravel_gap_valid_site(self): + """Test setting overtravel gap with valid ChuckSite.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_overtravel_gap(ChuckSite.AuxRight, 12.5) + self.mock_comm.send.assert_called_with("set_chuck_site_overtravel_gap AuxRight, 12.5") + self.assertIsNone(result) + + def test_set_chuck_site_pos_with_enum(self): + """Test set_chuck_site_pos with all values and ChuckSite enum.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_pos(1000.0, 2000.0, 10.0, ChuckSite.Wafer) + self.mock_comm.send.assert_called_with("set_chuck_site_pos Wafer,1000.0,2000.0,10.0") + self.assertIsNone(result) + + def test_set_chuck_site_pos_without_site(self): + """Test set_chuck_site_pos with x, y, theta only (uses current site).""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_pos(500.0, 500.0, 0.0) + self.mock_comm.send.assert_called_with("set_chuck_site_pos 500.0,500.0,0.0") + self.assertIsNone(result) + + def test_set_chuck_site_pos_with_no_arguments(self): + """Test set_chuck_site_pos with no arguments (uses current site and position).""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_pos() + self.mock_comm.send.assert_called_with("set_chuck_site_pos") + self.assertIsNone(result) + + def test_set_chuck_site_separation_gap_with_site(self): + """Test setting separation gap with ChuckSite enum.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_separation_gap(ChuckSite.AuxRight, 250.0) + self.mock_comm.send.assert_called_with("set_chuck_site_separation_gap AuxRight, 250.0") + self.assertIsNone(result) + + def test_set_high_power_air_on(self): + """Test setting high power air to ON.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_high_power_air(HighPowerAirState.On) + self.mock_comm.send.assert_called_with("set_high_power_air 1") + self.assertIsNone(result) + + def test_set_high_power_air_off(self): + """Test setting high power air to OFF.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_high_power_air(HighPowerAirState.Off) + self.mock_comm.send.assert_called_with("set_high_power_air 0") + self.assertIsNone(result) + + def test_set_high_power_air_none_raises(self): + """Test that passing None raises ValueError.""" + with self.assertRaises(ValueError): + self.test_prober.set_high_power_air(None) + + def test_set_soft_contact_enable(self): + """Test enabling soft contact.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_soft_contact(SoftContactState.Enable) + self.mock_comm.send.assert_called_with("set_soft_contact 1") + self.assertIsNone(result) + + def test_set_soft_contact_disable(self): + """Test disabling soft contact.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_soft_contact(SoftContactState.Disable) + self.mock_comm.send.assert_called_with("set_soft_contact 0") + self.assertIsNone(result) + + def test_set_soft_contact_none_raises(self): + """Test passing None raises ValueError.""" + with self.assertRaises(ValueError): + self.test_prober.set_soft_contact(None) + + def test_set_user_coordinate_origin_chuck(self): + """Test setting user coordinate origin for chuck.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_user_coordinate_origin(UserCoordState.Chuck, 0.0, 0.0) + self.mock_comm.send.assert_called_with("set_user_coordinate_origin chuck,0.0,0.0") + self.assertIsNone(result) + + def test_set_user_coordinate_origin_scope(self): + """Test setting user coordinate origin for scope.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_user_coordinate_origin(UserCoordState.Scope, 150.0, 300.0) + self.mock_comm.send.assert_called_with("set_user_coordinate_origin scope,150.0,300.0") + self.assertIsNone(result) + + def test_set_user_coordinate_origin_none_raises(self): + """Test that passing None as state raises ValueError.""" + with self.assertRaises(ValueError): + self.test_prober.set_user_coordinate_origin(None, 0.0, 0.0) + + def test_create_project_with_full_path(self): + """Test create_project using full directory path.""" + self.mock_comm.read_line.return_value = "0,0,C:\\Users\\Project1\\Project1.trex" + result = self.test_prober.create_project("C:\\Users\\Project1") + self.mock_comm.send.assert_called_with("create_project C:\\Users\\Project1") + self.assertEqual(result, "C:\\Users\\Project1\\Project1.trex") + + def test_create_project_with_name_only(self): + """Test create_project using just project name (default directory).""" + self.mock_comm.read_line.return_value = "0,0,Default project directory\\ProjectX\\ProjectX.trex" + result = self.test_prober.create_project("ProjectX") + self.mock_comm.send.assert_called_with("create_project ProjectX") + self.assertEqual(result, "Default project directory\\ProjectX\\ProjectX.trex") + if __name__ == "__main__": unittest.main() From 73430b6b55d324af8efc36accf0c86ef7ebe85f6 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Tue, 8 Apr 2025 12:15:08 +0800 Subject: [PATCH 10/41] =?UTF-8?q?Create=20=E6=96=B0=E6=96=87=E5=AD=97?= =?UTF-8?q?=E6=96=87=E4=BB=B6.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 "\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" diff --git "a/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" "b/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" new file mode 100644 index 0000000..e69de29 From ba21af2d523a4fac59befeaab404cb75abf0f2b5 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Tue, 8 Apr 2025 12:16:44 +0800 Subject: [PATCH 11/41] =?UTF-8?q?Delete=20=E6=96=B0=E6=96=87=E5=AD=97?= =?UTF-8?q?=E6=96=87=E4=BB=B6.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" diff --git "a/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" "b/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" deleted file mode 100644 index e69de29..0000000 From b92775670e5df8800a6e1089109df70777878f76 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 10 Apr 2025 09:32:34 +0800 Subject: [PATCH 12/41] Test --- ...6\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 "\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" diff --git "a/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" "b/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" new file mode 100644 index 0000000..e69de29 From 8b5b690b37d96f2dc4c85aea0a9273ee81d23151 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 10 Apr 2025 09:33:09 +0800 Subject: [PATCH 13/41] Test --- ...6\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" diff --git "a/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" "b/\346\226\260\346\226\207\345\255\227\346\226\207\344\273\266.txt" deleted file mode 100644 index e69de29..0000000 From a7c832a1d035e68a526c0779ca558616f970abc3 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 10 Apr 2025 10:28:00 +0800 Subject: [PATCH 14/41] Jeff add chuck and project handling commands --- sentio_prober_control/Sentio/Enumerations.py | 109 ++++++ sentio_prober_control/Sentio/ProberSentio.py | 365 +++++++++++++++++- .../UnitTest/TestProberSentio.py | 241 ++++++++++++ 3 files changed, 713 insertions(+), 2 deletions(-) diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index abcd789..d84712e 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -255,6 +255,20 @@ class ChuckPositionHint(Enum): Center = 0 FrontLoad = 1 SideLoad = 2 + OffAxisCamera = 3 + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Probing": ChuckPositionHint.Center, + "FrontLoad": ChuckPositionHint.FrontLoad, + "SideLoad": ChuckPositionHint.SideLoad, + "OffAxisCamera": ChuckPositionHint.OffAxisCamera + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckPositionHint abbreviation: {abbr}") class ChuckSite(Enum): @@ -270,6 +284,8 @@ class ChuckSite(Enum): AuxRight2 (3): Secondary right auxilliary site (if available) AuxLeft2 (4): Secondary left auxilliary site (if available) ChuckCamera (5): The chuck camera + SiPhSetHoverHeight (6): Siph set hover height site + SiPhFiberPowerMeasure (7): Siph fiber power measure site """ Wafer = 0 @@ -278,6 +294,8 @@ class ChuckSite(Enum): AuxRight2 = 3 AuxLeft2 = 4 ChuckCamera = 5 + SiPhSetHoverHeight = 6 + SiPhFiberPowerMeasure = 7 def toSentioAbbr(self): switcher = { @@ -287,9 +305,28 @@ def toSentioAbbr(self): ChuckSite.AuxRight: "AuxRight", ChuckSite.AuxRight2: "AuxRight2", ChuckSite.ChuckCamera: "ChuckCamera", + ChuckSite.SiPhSetHoverHeight: "SiPhSetHoverHeight", + ChuckSite.SiPhFiberPowerMeasure: "SiPhFiberPowerMeasure", } return switcher.get(self, "Invalid chuck site") + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Wafer": ChuckSite.Wafer, + "AuxRight": ChuckSite.AuxRight, + "AuxLeft": ChuckSite.AuxLeft, + "AuxRight2": ChuckSite.AuxRight2, + "AuxLeft2": ChuckSite.AuxLeft2, + "ChuckCamera": ChuckSite.ChuckCamera, + "SiPhSetHoverHeight": ChuckSite.SiPhSetHoverHeight, + "SiPhFiberPowerMeasure": ChuckSite.SiPhFiberPowerMeasure, + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckSite abbreviation: {abbr}") + class ChuckThetaReference(Enum): """Reference to use for chuck theat motions. @@ -1761,3 +1798,75 @@ def toSentioAbbr(self): HighPurgeState.Off: "OFF", } return switcher.get(self, "Invalid HighPurgeState") + +class ChuckSpeed(Enum): + Fast = 0 + Normal = 1 + Slow = 2 + Jog = 3 + Index = 4 + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Fast": ChuckSpeed.Fast, + "Normal": ChuckSpeed.Normal, + "Slow": ChuckSpeed.Slow, + "Jog": ChuckSpeed.Jog, + "Index": ChuckSpeed.Index, + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckSpeed abbreviation: {abbr}") + +class VacuumState(Enum): + Off = 0 + On = 1 + + def toSentioAbbr(self) -> str: + return { + VacuumState.Off: "Off", + VacuumState.On: "On", + }.get(self, "Invalid VacuumState") + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "0": VacuumState.Off, + "1": VacuumState.On + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown VacuumState abbreviation: {abbr}") + +class HighPowerAirState(Enum): + Off = 0 + On = 1 + + def toSentioAbbr(self) -> str: + return { + HighPowerAirState.Off: "0", + HighPowerAirState.On: "1", + }.get(self, "Invalid HighPowerAirState") + +class SoftContactState(Enum): + Disable = 0 + Enable = 1 + + def toSentioAbbr(self) -> str: + return { + SoftContactState.Disable: "0", + SoftContactState.Enable: "1", + }.get(self, "Invalid SoftContactState") + +class UserCoordState(Enum): + Chuck = 0 + Scope = 1 + + def toSentioAbbr(self) -> str: + return { + UserCoordState.Chuck: "chuck", + UserCoordState.Scope: "scope" + }.get(self, "Invalid UserCoordState") \ No newline at end of file diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index fc914ff..8904da9 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -19,7 +19,13 @@ Stage, SteppingContactMode, VceZReference, - WorkArea + WorkArea, + ChuckPositionHint, + ChuckSpeed, + VacuumState, + HighPowerAirState, + SoftContactState, + UserCoordState ) from sentio_prober_control.Sentio.ProberBase import ProberBase, ProberException @@ -1144,4 +1150,359 @@ def step_scope_site_next(self) -> tuple[str, float, float]: offset_x = float(tok[1]) offset_y = float(tok[2]) - return site_id, offset_x, offset_y \ No newline at end of file + return site_id, offset_x, offset_y + + def get_chuck_position_hint(self) -> tuple[ChuckPositionHint, ChuckSite]: + """Get a verbal representation of the current chuck position and the active site. + + Returns: + position_hint (str): Describes the chuck's general position, e.g., "Probing", "FrontLoad", "SideLoad". + site (str): The current chuck site, e.g., "Wafer", "AuxRight", "ChuckCameraLR", etc. + """ + self.comm.send("get_chuck_position_hint") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + pos_hint_str = tok[0] + site_str = tok[1] + + pos_hint = ChuckPositionHint.fromSentioAbbr(pos_hint_str) + site = ChuckSite.fromSentioAbbr(site_str) + + return pos_hint, site + + def get_chuck_site_count(self) -> int: + """Retrieve the number of chuck sites. + + Wraps SENTIO's "get_chuck_site_count" remote command. + + Returns: + count (int): The number of chuck sites. + """ + self.comm.send("get_chuck_site_count") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + count = int(tok[0]) + + return count + + def get_chuck_site_index(self, name: str | None = None) -> int: + """Retrieve the index of a chuck site by name. If no name is provided, returns the index of the current site. + + Args: + name (str, optional): The name of the chuck site. If None, the current site index is returned. + + Returns: + index (int): The index of the chuck site. + """ + if name is None: + self.comm.send("get_chuck_site_index") + else: + self.comm.send(f"get_chuck_site_index {name}") + + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + index = int(tok[0]) + + return index + + def get_chuck_site_name(self, index: int | None = None) -> ChuckSite: + """Retrieve the name of a chuck site. If no index is given, returns the current site's name. + + Args: + index (int, optional): The index of the chuck site. If None, gets the name of the current site. + + Returns: + name (str): The name of the chuck site. + """ + if index is None: + self.comm.send("get_chuck_site_name") + else: + self.comm.send(f"get_chuck_site_name {index}") + + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_str = tok[0] + site = ChuckSite.fromSentioAbbr(site_str) + + return site + + def get_chuck_site_pos(self, site: ChuckSite | None = None) -> tuple[float, float, float]: + """Retrieve position information of a chuck site. + + Args: + site (ChuckSite, optional): The chuck site to query. If None, uses the current active site. + + Returns: + tuple[float, float, float]: (home_x, home_y, angle) in µm and degrees. + """ + if site is not None: + site_arg = site.toSentioAbbr() + self.comm.send(f"get_chuck_site_pos {site_arg}") + else: + self.comm.send("get_chuck_site_pos") + + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + home_x = float(tok[0]) + home_y = float(tok[1]) + angle = float(tok[2]) + return home_x, home_y, angle + + def get_chuck_speed(self) -> ChuckSpeed: + """Retrieve the current chuck speed setting. + + Returns: + ChuckSpeed: Enum representing current chuck speed. + """ + self.comm.send("get_chuck_speed") + resp = Response.check_resp(self.comm.read_line()) + speed_str = resp.message().split(",")[0] + + speed = ChuckSpeed.fromSentioAbbr(speed_str) + + return speed + + from sentio_prober_control.Sentio.Enumerations import ChuckSite + + def get_vacuum_status(self, site: ChuckSite | None = None) -> VacuumState: + """Get the vacuum status of a chuck site. + + Args: + site (ChuckSite, optional): The chuck site to query. If None, queries the currently active site. + + Returns: + VacuumState: Enum indicating On or Off. + """ + if site is not None: + site_arg = site.toSentioAbbr() + self.comm.send(f"get_vacuum_status {site_arg}") + else: + self.comm.send("get_vacuum_status") + + resp = Response.check_resp(self.comm.read_line()) + status = resp.message().split(",")[0] + + return VacuumState.fromSentioAbbr(status) + + def move_chuck_hover(self) -> float: + """Move the chuck to hover height. + + If hover height is not enabled, the move will not be carried out. + + Returns: + z (float): The new Z position in micrometers with respect to zero. + """ + self.comm.send("move_chuck_hover") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + z_pos = float(tok[0]) + + return z_pos + + def move_chuck_index(self, ref: str, x_steps: int, y_steps: int) -> tuple[float, float]: + """Move chuck xy by a number of index steps relative to a reference point. + + If the chuck is above separation height, it will move to separation before and return after movement. + + Args: + ref (str): The reference point ("Zero", "Home", "Relative", "Center"). Case-insensitive. + x_steps (int): Number of steps in X direction. + y_steps (int): Number of steps in Y direction. + + Returns: + new_x (float): New X position (in µm, from axis zero). + new_y (float): New Y position (in µm, from axis zero). + """ + self.comm.send(f"move_chuck_index {ref}, {x_steps}, {y_steps}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + new_x = float(tok[0]) + new_y = float(tok[1]) + + return new_x, new_y + + def move_chuck_xyt(self, x_offset: float, y_offset: float, theta_offset: float) -> None: + """Move chuck xy and theta to match pattern to field of view center. + + If chuck is above separation height, it will move to separation first and stay there after movement. + + Args: + x_offset (float): Offset in X direction (in µm). + y_offset (float): Offset in Y direction (in µm). + theta_offset (float): Offset in theta (in degrees). + + Returns: + result (str): Usually returns "OK" on success. + """ + self.comm.send(f"move_chuck_xyt {x_offset}, {y_offset}, {theta_offset}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_overtravel_gap(self, gap: float) -> None: + """Set overtravel gap for all chuck sites. + + Args: + gap (float): The overtravel gap in micrometers. + + Returns: + result (str): Typically "OK" if successfully set. + """ + self.comm.send(f"set_chuck_overtravel_gap {gap}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_separation_gap(self, gap: float) -> None: + """Set separation gap for all chuck sites. + + Args: + gap (float): The separation gap in micrometers. + + Returns: + result (str): Typically "OK" if successfully set. + """ + self.comm.send(f"set_chuck_separation_gap {gap}") + Response.check_resp(self.comm.read_line()) + + from sentio_prober_control.Sentio.Enumerations import ChuckSite + from sentio_prober_control.Sentio.Response import Response + + def set_chuck_site_overtravel_gap(self, site: ChuckSite, gap: float) -> None: + """Set overtravel gap for a specific chuck site. + + Args: + site (ChuckSite): The chuck site to apply the overtravel gap to. Cannot be None. + gap (float): Overtravel gap in micrometers. + + Raises: + ValueError: If site is None. + """ + if site is None: + raise ValueError("site is required and cannot be None") + + site_arg = site.toSentioAbbr() + self.comm.send(f"set_chuck_site_overtravel_gap {site_arg}, {gap}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_site_pos( + self, + x: float | None = None, + y: float | None = None, + theta: float | None = None, + site: ChuckSite | None = None + ) -> None: + """Set home position information of a chuck site. + + Args: + x (float, optional): Home X position in µm. + y (float, optional): Home Y position in µm. + theta (float, optional): Home angle in degrees. + site (ChuckSite, optional): Site enum. If None, uses current chuck site. + + Returns: + None + """ + if x is not None and y is not None and theta is not None: + site_arg = site.toSentioAbbr() if site is not None else None + + if site_arg: + self.comm.send(f"set_chuck_site_pos {site_arg},{x},{y},{theta}") + else: + self.comm.send(f"set_chuck_site_pos {x},{y},{theta}") + else: + self.comm.send("set_chuck_site_pos") + + Response.check_resp(self.comm.read_line()) + + def set_chuck_site_separation_gap(self, site: ChuckSite, gap: float) -> None: + """Set separation gap for a specific chuck site. + + Args: + site (ChuckSite): The chuck site to apply the gap to. This parameter is required. + gap (float): Separation gap in micrometers. + + Raises: + ValueError: If site is None. + """ + if site is None: + raise ValueError("site is required and cannot be None") + + site_arg = site.toSentioAbbr() + self.comm.send(f"set_chuck_site_separation_gap {site_arg}, {gap}") + Response.check_resp(self.comm.read_line()) + + def set_high_power_air(self, state: HighPowerAirState) -> None: + """Switch the air of high power prober card on or off. + + Args: + state (HighPowerAirState): Enum value indicating whether to enable or disable air. + + Raises: + ValueError: If state is None or not a valid enum. + """ + if state is None: + raise ValueError("state is required and must be a valid HighPowerAirState enum") + + self.comm.send(f"set_high_power_air {state.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_soft_contact(self, state: SoftContactState) -> None: + """Enable or disable soft contact mode. + + Args: + state (SoftContactState): Enum indicating whether to enable or disable soft contact. + + Raises: + ValueError: If state is None. + """ + if state is None: + raise ValueError("state is required and must be a valid SoftContactState enum") + + self.comm.send(f"set_soft_contact {state.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_user_coordinate_origin(self, state: UserCoordState, x: float, y: float) -> None: + """Set the new user coordinate origin (X, Y) for chuck or scope. + + Args: + state (UserCoordState): Either UserCoordState.Chuck or UserCoordState.Scope. + x (float): X offset in micrometers. + y (float): Y offset in micrometers. + + Raises: + ValueError: If any parameter is invalid. + """ + if state is None: + raise ValueError("state is required and must be a valid UserCoordState") + + cmd = f"set_user_coordinate_origin {state.toSentioAbbr()},{x},{y}" + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + def create_project(self, path_or_name: str) -> str: + """Create a new project. Overwrites if a project with the same name exists. + + Args: + path_or_name (str): Full path or project name. + If only a name is given, it will be created in the default project directory. + + Returns: + full_path (str): The full path to the created project file (*.trex). + """ + self.comm.send(f"create_project {path_or_name}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + full_path = tok[0] + + return full_path diff --git a/sentio_prober_control/UnitTest/TestProberSentio.py b/sentio_prober_control/UnitTest/TestProberSentio.py index 3a81988..fa75d64 100644 --- a/sentio_prober_control/UnitTest/TestProberSentio.py +++ b/sentio_prober_control/UnitTest/TestProberSentio.py @@ -4,6 +4,16 @@ from sentio_prober_control.Sentio.ProberSentio import SentioProber from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.Enumerations import ( + ChuckPositionHint, + ChuckSite, + ChuckSpeed, + VacuumState, + HighPowerAirState, + SoftContactState, + UserCoordState +) + class TestScopeCommandGroup(unittest.TestCase): def setUp(self): """Initialize the mock communicator and ScopeCommandGroup instance.""" @@ -61,6 +71,237 @@ def test_step_scope_site_next(self): self.assertEqual(offset_x, 1000.0) self.assertEqual(offset_y, 1000.0) + def test_get_chuck_position_hint(self): + """Test get_chuck_position_hint returns correct enums.""" + self.mock_comm.read_line.return_value = "0,0,Probing,Wafer" + position, site = self.test_prober.get_chuck_position_hint() + self.mock_comm.send.assert_called_with("get_chuck_position_hint") + self.assertEqual(position, ChuckPositionHint.Center) + self.assertEqual(site, ChuckSite.Wafer) + + def test_get_chuck_site_count(self): + """Test get_chuck_site_count returns correct integer.""" + self.mock_comm.read_line.return_value = "0,0,5" + count = self.test_prober.get_chuck_site_count() + self.mock_comm.send.assert_called_with("get_chuck_site_count") + self.assertEqual(count, 5) + + def test_get_chuck_site_index_without_name(self): + """Test get_chuck_site_index without argument returns correct index.""" + self.mock_comm.read_line.return_value = "0,0,2" + index = self.test_prober.get_chuck_site_index() + self.mock_comm.send.assert_called_with("get_chuck_site_index") + self.assertEqual(index, 2) + + def test_get_chuck_site_index_with_name(self): + """Test get_chuck_site_index with a site name returns correct index.""" + self.mock_comm.read_line.return_value = "0,0,3" + index = self.test_prober.get_chuck_site_index("Wafer") + self.mock_comm.send.assert_called_with("get_chuck_site_index Wafer") + self.assertEqual(index, 3) + + def test_get_chuck_site_name_without_index(self): + """Test get_chuck_site_name with no index.""" + self.mock_comm.read_line.return_value = "0,0,Wafer" + result = self.test_prober.get_chuck_site_name() + self.mock_comm.send.assert_called_with("get_chuck_site_name") + self.assertEqual(result, ChuckSite.Wafer) + + def test_get_chuck_site_name_with_index(self): + """Test get_chuck_site_name with specific index.""" + self.mock_comm.read_line.return_value = "0,0,AuxLeft" + result = self.test_prober.get_chuck_site_name(2) + self.mock_comm.send.assert_called_with("get_chuck_site_name 2") + self.assertEqual(result, ChuckSite.AuxLeft) + + def test_get_chuck_site_pos_with_site(self): + """Test get_chuck_site_pos with ChuckSite enum.""" + self.mock_comm.read_line.return_value = "0,0,25000,40000,-45" + x, y, theta = self.test_prober.get_chuck_site_pos(ChuckSite.Wafer) + self.mock_comm.send.assert_called_with("get_chuck_site_pos Wafer") + self.assertEqual(x, 25000.0) + self.assertEqual(y, 40000.0) + self.assertEqual(theta, -45.0) + + def test_get_chuck_site_pos_without_site(self): + """Test get_chuck_site_pos without site (use current active site).""" + self.mock_comm.read_line.return_value = "0,0,1000,2000,15" + x, y, theta = self.test_prober.get_chuck_site_pos() + self.mock_comm.send.assert_called_with("get_chuck_site_pos") + self.assertEqual(x, 1000.0) + self.assertEqual(y, 2000.0) + self.assertEqual(theta, 15.0) + + def test_get_chuck_speed(self): + """Test get_chuck_speed returns correct ChuckSpeed enum.""" + self.mock_comm.read_line.return_value = "0,0,Fast" + result = self.test_prober.get_chuck_speed() + self.mock_comm.send.assert_called_with("get_chuck_speed") + self.assertEqual(result, ChuckSpeed.Fast) + + def test_get_vacuum_status_on_with_site(self): + """Test get_vacuum_status returns ON with ChuckSite specified.""" + self.mock_comm.read_line.return_value = "0,0,1" + result = self.test_prober.get_vacuum_status(ChuckSite.AuxLeft) + self.mock_comm.send.assert_called_with("get_vacuum_status AuxLeft") + self.assertEqual(result, VacuumState.On) + + def test_get_vacuum_status_off_with_site(self): + """Test get_vacuum_status returns OFF with ChuckSite specified.""" + self.mock_comm.read_line.return_value = "0,0,0" + result = self.test_prober.get_vacuum_status(ChuckSite.Wafer) + self.mock_comm.send.assert_called_with("get_vacuum_status Wafer") + self.assertEqual(result, VacuumState.Off) + + def test_get_vacuum_status_without_site(self): + """Test get_vacuum_status returns ON using current active site.""" + self.mock_comm.read_line.return_value = "0,0,1" + result = self.test_prober.get_vacuum_status() + self.mock_comm.send.assert_called_with("get_vacuum_status") + self.assertEqual(result, VacuumState.On) + + def test_move_chuck_hover(self): + """Test move_chuck_hover returns correct Z position.""" + self.mock_comm.read_line.return_value = "0,0,16000.0" + z = self.test_prober.move_chuck_hover() + self.mock_comm.send.assert_called_with("move_chuck_hover") + self.assertEqual(z, 16000.0) + + def test_move_chuck_index(self): + """Test move_chuck_index returns correct new XY position.""" + self.mock_comm.read_line.return_value = "0,0,1000.0,3000.0" + x, y = self.test_prober.move_chuck_index("Home", 5, -3) + self.mock_comm.send.assert_called_with("move_chuck_index Home, 5, -3") + self.assertEqual(x, 1000.0) + self.assertEqual(y, 3000.0) + + def test_move_chuck_xyt(self): + """Test move_chuck_xyt sends correct command and completes without error.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.move_chuck_xyt(5.0, -10.0, 0.5) + self.mock_comm.send.assert_called_with("move_chuck_xyt 5.0, -10.0, 0.5") + self.assertIsNone(result) + + def test_set_chuck_overtravel_gap(self): + """Test set_chuck_overtravel_gap sends correct command and succeeds.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_overtravel_gap(50.0) + self.mock_comm.send.assert_called_with("set_chuck_overtravel_gap 50.0") + self.assertIsNone(result) + + def test_set_chuck_separation_gap(self): + """Test set_chuck_separation_gap sends correct command and succeeds.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_separation_gap(250.0) + self.mock_comm.send.assert_called_with("set_chuck_separation_gap 250.0") + self.assertIsNone(result) + + def test_set_chuck_site_overtravel_gap_valid_site(self): + """Test setting overtravel gap with valid ChuckSite.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_overtravel_gap(ChuckSite.AuxRight, 12.5) + self.mock_comm.send.assert_called_with("set_chuck_site_overtravel_gap AuxRight, 12.5") + self.assertIsNone(result) + + def test_set_chuck_site_pos_with_enum(self): + """Test set_chuck_site_pos with all values and ChuckSite enum.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_pos(1000.0, 2000.0, 10.0, ChuckSite.Wafer) + self.mock_comm.send.assert_called_with("set_chuck_site_pos Wafer,1000.0,2000.0,10.0") + self.assertIsNone(result) + + def test_set_chuck_site_pos_without_site(self): + """Test set_chuck_site_pos with x, y, theta only (uses current site).""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_pos(500.0, 500.0, 0.0) + self.mock_comm.send.assert_called_with("set_chuck_site_pos 500.0,500.0,0.0") + self.assertIsNone(result) + + def test_set_chuck_site_pos_with_no_arguments(self): + """Test set_chuck_site_pos with no arguments (uses current site and position).""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_pos() + self.mock_comm.send.assert_called_with("set_chuck_site_pos") + self.assertIsNone(result) + + def test_set_chuck_site_separation_gap_with_site(self): + """Test setting separation gap with ChuckSite enum.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_chuck_site_separation_gap(ChuckSite.AuxRight, 250.0) + self.mock_comm.send.assert_called_with("set_chuck_site_separation_gap AuxRight, 250.0") + self.assertIsNone(result) + + def test_set_high_power_air_on(self): + """Test setting high power air to ON.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_high_power_air(HighPowerAirState.On) + self.mock_comm.send.assert_called_with("set_high_power_air 1") + self.assertIsNone(result) + + def test_set_high_power_air_off(self): + """Test setting high power air to OFF.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_high_power_air(HighPowerAirState.Off) + self.mock_comm.send.assert_called_with("set_high_power_air 0") + self.assertIsNone(result) + + def test_set_high_power_air_none_raises(self): + """Test that passing None raises ValueError.""" + with self.assertRaises(ValueError): + self.test_prober.set_high_power_air(None) + + def test_set_soft_contact_enable(self): + """Test enabling soft contact.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_soft_contact(SoftContactState.Enable) + self.mock_comm.send.assert_called_with("set_soft_contact 1") + self.assertIsNone(result) + + def test_set_soft_contact_disable(self): + """Test disabling soft contact.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_soft_contact(SoftContactState.Disable) + self.mock_comm.send.assert_called_with("set_soft_contact 0") + self.assertIsNone(result) + + def test_set_soft_contact_none_raises(self): + """Test passing None raises ValueError.""" + with self.assertRaises(ValueError): + self.test_prober.set_soft_contact(None) + + def test_set_user_coordinate_origin_chuck(self): + """Test setting user coordinate origin for chuck.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_user_coordinate_origin(UserCoordState.Chuck, 0.0, 0.0) + self.mock_comm.send.assert_called_with("set_user_coordinate_origin chuck,0.0,0.0") + self.assertIsNone(result) + + def test_set_user_coordinate_origin_scope(self): + """Test setting user coordinate origin for scope.""" + self.mock_comm.read_line.return_value = "0,0,OK" + result = self.test_prober.set_user_coordinate_origin(UserCoordState.Scope, 150.0, 300.0) + self.mock_comm.send.assert_called_with("set_user_coordinate_origin scope,150.0,300.0") + self.assertIsNone(result) + + def test_set_user_coordinate_origin_none_raises(self): + """Test that passing None as state raises ValueError.""" + with self.assertRaises(ValueError): + self.test_prober.set_user_coordinate_origin(None, 0.0, 0.0) + + def test_create_project_with_full_path(self): + """Test create_project using full directory path.""" + self.mock_comm.read_line.return_value = "0,0,C:\\Users\\Project1\\Project1.trex" + result = self.test_prober.create_project("C:\\Users\\Project1") + self.mock_comm.send.assert_called_with("create_project C:\\Users\\Project1") + self.assertEqual(result, "C:\\Users\\Project1\\Project1.trex") + + def test_create_project_with_name_only(self): + """Test create_project using just project name (default directory).""" + self.mock_comm.read_line.return_value = "0,0,Default project directory\\ProjectX\\ProjectX.trex" + result = self.test_prober.create_project("ProjectX") + self.mock_comm.send.assert_called_with("create_project ProjectX") + self.assertEqual(result, "Default project directory\\ProjectX\\ProjectX.trex") + if __name__ == "__main__": unittest.main() From 328ffff86fb923ef49acef6c6ce3aacd0cdddbe9 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 10 Apr 2025 10:42:26 +0800 Subject: [PATCH 15/41] add Cynthia vision API --- .../CommandGroups/VisionCommandGroup.py | 119 ++++++++---------- .../VisionPatternCommandGroup.py | 75 +++++++++++ sentio_prober_control/Sentio/Enumerations.py | 28 +++++ 3 files changed, 157 insertions(+), 65 deletions(-) create mode 100644 sentio_prober_control/Sentio/CommandGroups/VisionPatternCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py index dd1fdcb..3f3272a 100644 --- a/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py @@ -16,14 +16,14 @@ PtpaFindTipsMode, PtpaType, SnapshotLocation, - SnapshotType, -) + SnapshotType, MoveAxis,) from sentio_prober_control.Sentio.ProberBase import ProberException from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ModuleCommandGroupBase from sentio_prober_control.Sentio.CommandGroups.VisionCameraCommandGroup import VisionCameraCommandGroup from sentio_prober_control.Sentio.CommandGroups.VisionCompensationGroup import VisionCompensationGroup from sentio_prober_control.Sentio.CommandGroups.VisionIMagProCommandGroup import VisionIMagProCommandGroup +from sentio_prober_control.Sentio.CommandGroups.VisionPatternCommandGroup import VisionPatternCommandGroup class VisionCommandGroup(ModuleCommandGroupBase): @@ -43,7 +43,7 @@ def __init__(self, comm: CommunicatorBase) -> None: self.camera = VisionCameraCommandGroup(comm) self.imagpro = VisionIMagProCommandGroup(comm) self.compensation = VisionCompensationGroup(comm) - + self.pattern = VisionPatternCommandGroup(comm) def align_wafer(self, mode: AutoAlignCmd = AutoAlignCmd.AlignOnly) -> None: """Perform a wafer alignment. @@ -65,7 +65,6 @@ def align_wafer(self, mode: AutoAlignCmd = AutoAlignCmd.AlignOnly) -> None: Response.check_resp(self.comm.read_line()) - def align_die(self, threshold: float = 0.05) -> Tuple[float, float, float]: """Perform a die alignment. @@ -84,8 +83,7 @@ def align_die(self, threshold: float = 0.05) -> Tuple[float, float, float]: tok = resp.message().split(",") return float(tok[0]), float(tok[1]), float(tok[2]) - - def auto_focus(self, af_cmd: AutoFocusCmd = AutoFocusCmd.Focus) -> float: + def auto_focus(self, af_cmd: AutoFocusCmd = AutoFocusCmd.Focus) -> tuple[float, MoveAxis]: """Perform an auto focus operation. Args: @@ -94,11 +92,9 @@ def auto_focus(self, af_cmd: AutoFocusCmd = AutoFocusCmd.Focus) -> float: Returns: The focus height in micrometer """ - resp = self.prober.send_cmd(f"vis:auto_focus {af_cmd.toSentioAbbr()}") tok = resp.message().split(",") - return float(tok[0]) - + return float(tok[0]), MoveAxis[tok[1].capitalize()] def camera_synchronize(self) -> Tuple[float, float, float]: self.comm.send("vis:camera_synchronize") @@ -106,29 +102,6 @@ def camera_synchronize(self) -> Tuple[float, float, float]: tok = resp.message().split(",") return float(tok[0]), float(tok[1]), float(tok[2]) - - def create_probepad_model(self, angleStep: float = 0.1, imgPath: str | None = None, UL: tuple | None = None, LR: tuple | None = None): - if imgPath == None or UL == None or LR == None: - self.comm.send(f"vis:create_probepad_model {angleStep}") - else: - self.comm.send(f"vis:create_probepad_model {angleStep}, {imgPath}, {UL[0]}, {UL[1]}, {LR[0]}, {LR[1]}") - - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - return tok - - - def detect_probepads(self, imgPath: str | None = None, minScore: float = 0.7, startAngle: float | None = None, startExtend: float | None = None, maxOverlap: float | None = None) -> Tuple[bool, float, str]: - if startAngle == None: - self.comm.send("vis:detect_probepads {},{}".format(imgPath, minScore)) - else: - self.comm.send("vis:detect_probepads {},{},{},{},{}".format(imgPath, minScore, startAngle, startExtend, maxOverlap)) - - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - return bool(tok[0]), float(tok[1]), tok[2] - - def detect_probetips(self, camera: CameraMountPoint, detector: DetectionAlgorithm = DetectionAlgorithm.ProbeDetector, coords: DetectionCoordindates = DetectionCoordindates.Roi) -> list: """Executes a built in detector on a given camera and return a list of detection results. @@ -164,8 +137,7 @@ def detect_probetips(self, camera: CameraMountPoint, detector: DetectionAlgorith return found_tips - - def enable_follow_mode(self, stat: bool) -> Response: + def enable_follow_mode(self, stat: bool): """Enable or disable the scope follow mode. If scope follow mode is active the scope will move in sync with the chuck. This is useful for @@ -175,27 +147,19 @@ def enable_follow_mode(self, stat: bool) -> Response: Args: stat: A flag indicating whether to enable or disable the follow mode. - - Returns: - A Response object. """ self.comm.send("vis:enable_follow_mode {0}".format(stat)) - return Response.check_resp(self.comm.read_line()) - + Response.check_resp(self.comm.read_line()) - def find_home(self) -> Response: + def find_home(self): """Find home position. This function uses a pre-trained pattern to fully automatically find the home position. - - Returns: - A Response object. """ self.comm.send("vis:find_home") - return Response.check_resp(self.comm.read_line()) - + Response.check_resp(self.comm.read_line()) def find_pattern(self, name: str, threshold: float = 70, pattern_index: int = 0, reference: FindPatternReference = FindPatternReference.CenterOfRoi) -> Tuple[float, float, float, float]: """Find a trained pattern in the camera image. @@ -212,7 +176,6 @@ def find_pattern(self, name: str, threshold: float = 70, pattern_index: int = 0, tok = resp.message().split(",") return float(tok[0]), float(tok[1]), float(tok[2]), float(tok[3]) - def has_camera(self, camera: CameraMountPoint) -> bool: """Check wether a given camera is present in the system. @@ -229,8 +192,7 @@ def has_camera(self, camera: CameraMountPoint) -> bool: resp = Response.check_resp(self.comm.read_line()) return resp.message().upper() == "1" - - def switch_all_lights(self, stat: bool) -> Response: + def switch_all_lights(self, stat: bool) -> None: """Switch all camera lights on or off. This function wraps the "vis:switch_all_lights" remote command. @@ -243,8 +205,6 @@ def switch_all_lights(self, stat: bool) -> Response: """ self.comm.send("vis:switch_all_lights {0}".format(stat)) - return Response.check_resp(self.comm.read_line()) - def remove_probetip_marker(self) -> Response: """Remove probetip marker from the camera display. @@ -256,7 +216,6 @@ def remove_probetip_marker(self) -> Response: self.comm.send("vis:remove_probetip_marker") return Response.check_resp(self.comm.read_line()) - def match_tips(self, ptpa_type: PtpaType) -> Tuple[float, float]: """For internal use only! This function is subject to change without any prior warning. MPI will not maintain backwards @@ -267,7 +226,6 @@ def match_tips(self, ptpa_type: PtpaType) -> Tuple[float, float]: tok = resp.message().split(",") return float(tok[0]), float(tok[1]) - def snap_image(self, file: str, what: SnapshotType = SnapshotType.CameraRaw, where: SnapshotLocation = SnapshotLocation.Prober) -> None: """Save a snapshot of the current camera image to a file. @@ -293,8 +251,7 @@ def snap_image(self, file: str, what: SnapshotType = SnapshotType.CameraRaw, whe self.comm.send(f"vis:snap_image {file}, {what.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) - - def switch_light(self, camera: CameraMountPoint, stat: bool) -> Response: + def switch_light(self, camera: CameraMountPoint, stat: bool): """Switch the light of a given camera on or off. Args: @@ -304,12 +261,10 @@ def switch_light(self, camera: CameraMountPoint, stat: bool) -> Response: Returns: A Response object. """ - self.comm.send(f"vis:switch_light {camera.toSentioAbbr()}, {stat}") - return Response.check_resp(self.comm.read_line()) - + Response.check_resp(self.comm.read_line()) - def switch_camera(self, camera: CameraMountPoint) -> Response: + def switch_camera(self, camera: CameraMountPoint): """Switch the camera to use for the vision module. Args: @@ -320,8 +275,6 @@ def switch_camera(self, camera: CameraMountPoint) -> Response: """ self.comm.send(f"vis:switch_camera {camera.toSentioAbbr()}") - return Response.check_resp(self.comm.read_line()) - def ptpa_find_pads(self, row: int = 0, column: int = 0): self.comm.send("vis:execute_ptpa_find_pads {0},{1}".format(row, column)) @@ -329,14 +282,12 @@ def ptpa_find_pads(self, row: int = 0, column: int = 0): tok = resp.message().split(",") return float(tok[0]), float(tok[1]), float(tok[2]) - def ptpa_find_tips(self, ptpa_mode: PtpaFindTipsMode): self.comm.send("vis:ptpa_find_tips {0}".format(ptpa_mode.toSentioAbbr())) resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") return float(tok[0]), float(tok[1]), float(tok[2]) - def start_fast_track(self) -> Response: """Start the fast track process as defined in SENTIO. @@ -350,13 +301,51 @@ def start_fast_track(self) -> Response: return self.prober.send_cmd("vis:start_fast_track") - @deprecated("use vision.compensation.start_execute(...) instead!") - def start_execute_compensation(self, comp_type: DieCompensationType, comp_mode: DieCompensationMode): + def start_execute_compensation(self, comp_type: DieCompensationType, comp_mode: DieCompensationMode) -> Response: self.comm.send("vis:compensation:start_execute {0},{1}".format(comp_type.toSentioAbbr(), comp_mode.toSentioAbbr())) resp = Response.check_resp(self.comm.read_line()) if not resp.ok(): raise ProberException(resp.message()) - return resp.cmd_id() + return resp + + def find_thermal_die_size(self) -> Tuple[float, float]: + """Detect thermal expansion and return die size ratio. + + Returns: + A tuple of (ThermalFactorX, ThermalFactorY) + """ + self.comm.send("vis:find_thermal_die_size") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + def get_lens_zoom_level(self) -> float: + """Get current zoom level of the lens.""" + self.comm.send("vis:get_lens_zoom_level") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + def set_lens_zoom_level(self, level: float) -> None: + """Set lens zoom level. + + Args: + level (float): Zoom level, such as 1, 2, ..., 10 + """ + self.comm.send(f"vis:set_lens_zoom_level {level}") + Response.check_resp(self.comm.read_line()) + + def get_light_status(self, camera: str) -> bool: + """Check whether light is on or off for a specific camera. + + Args: + camera: e.g. 'scope', 'offaxis' + + Returns: + True if light is ON + """ + self.comm.send(f"vis:get_light_status {camera}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message().strip().lower() == "1" diff --git a/sentio_prober_control/Sentio/CommandGroups/VisionPatternCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/VisionPatternCommandGroup.py new file mode 100644 index 0000000..eaef054 --- /dev/null +++ b/sentio_prober_control/Sentio/CommandGroups/VisionPatternCommandGroup.py @@ -0,0 +1,75 @@ +from typing import Tuple + +from sentio_prober_control.Sentio.Enumerations import BinSelection, BinQuality, FindPatternReference, CameraMountPoint, \ + DefaultPattern +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase + + +class VisionPatternCommandGroup(CommandGroupBase): + """This command group bundles functions for setting up and using the pattern.""" + + def find(self, name: str, threshold: float = 70, pattern_index: int = 0, reference: FindPatternReference = FindPatternReference.CenterOfRoi) -> Tuple[float, float, float, float]: + """Find a trained pattern in the camera image. + + Args: + name: The name of the pattern to find. + threshold: The detection threshold. The higher the threshold, the more certain the detection must be. + pattern_index: The index of the pattern to find. In SENTIO each pattern may have up to 5 alternate patterns. This is the index of the alternate pattern. + reference: The reference point to use for the pattern detection. + """ + + self.comm.send(f"vis:find_pattern {name}, {threshold}, {pattern_index}, {reference.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]), float(tok[2]), float(tok[3]) + + def get_chuck_pos(self, camera: CameraMountPoint, pattern: DefaultPattern) -> Tuple[float, float]: + """Get the chuck XY position associated with a trained pattern. + + Args: + camera: The camera mount point (e.g., Scope). + pattern: The name of the trained pattern. + + Returns: + A tuple with the X and Y coordinates in micrometers. + """ + self.comm.send(f"vis:pattern:get_chuck_pos {camera.toSentioAbbr()}, {pattern.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + def set_chuck_pos(self, camera: CameraMountPoint, pattern: DefaultPattern, x: float, y: float) -> Tuple[float, float]: + """Set the chuck XY position associated with a trained pattern. + + Args: + camera: The camera mount point (e.g., Scope). + pattern: The name of the pattern. + x: The X coordinate to assign in micrometers. + y: The Y coordinate to assign in micrometers. + + Returns: + A tuple with the confirmed X and Y coordinates. + """ + self.comm.send(f"vis:pattern:set_chuck_pos {camera.toSentioAbbr()}, {pattern.toSentioAbbr()}, {x}, {y}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + def show_training_box(self, visible: bool = True) -> None: + """Show or hide the pattern training box on the vision UI. + + Args: + visible: True to show the box, False to hide it. + """ + self.comm.send(f"vis:pattern:show_training_box {str(visible).lower()}") + Response.check_resp(self.comm.read_line()) + + def train(self, pattern: str) -> None: + """Train a new pattern using the current training box. + + Args: + pattern: The name of the pattern to store. + """ + self.comm.send(f"vis:pattern:train {pattern}") + Response.check_resp(self.comm.read_line()) \ No newline at end of file diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index d84712e..2434530 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -42,6 +42,8 @@ class AutoFocusAlgorithm(Enum): Bandpass = 1 Difference = 2 AutoCorrelation = 3 + LaplaceStdDev = 4 + Harris = 5 def toSentioAbbr(self): switcher = { @@ -49,6 +51,8 @@ def toSentioAbbr(self): AutoFocusAlgorithm.Bandpass: "Bandpass", AutoFocusAlgorithm.Difference: "Difference", AutoFocusAlgorithm.AutoCorrelation: "AutoCorrelation", + AutoFocusAlgorithm.LaplaceStdDev: "LaplaceStdDev", + AutoFocusAlgorithm.Harris: "Harris", } return switcher.get(self, "Invalid focus measure") @@ -75,6 +79,22 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid auto focus function") +class MoveAxis(Enum): + """Defines the movement axis for the auto-focus function.""" + + Scope = 0 + Imagpro = 1 + Chuck = 2 + + def toSentioAbbr(self): + switcher = { + MoveAxis.Scope: "scope", + MoveAxis.Imagpro: "imagpro", + MoveAxis.Chuck: "chuck", + } + return switcher.get(self, "Invalid AxisOrient") + + class AxisOrient(Enum): """Represents an axis orientation. @@ -730,12 +750,20 @@ class DieCompensationType(Enum): DieAlign = 0 MapScan = 1 Topography = 2 + AlignDie = 3 + ContactSense = 4 + OnTheFly = 5 + Offaxis = 6 def toSentioAbbr(self): switcher = { DieCompensationType.DieAlign: "DieAlign", DieCompensationType.MapScan: "MapScan", DieCompensationType.Topography: "Topography", + DieCompensationType.AlignDie: "AlignDie", + DieCompensationType.ContactSense: "ContactSense", + DieCompensationType.OnTheFly: "OnTheFly", + DieCompensationType.Offaxis: "Offaxis", } return switcher.get(self, "Invalid Compensation_Type function") From deecc1a49e97b3dea1c4fb38017db8462861391d Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 10 Apr 2025 10:59:44 +0800 Subject: [PATCH 16/41] add Cynthia vision unit test --- .../CommandGroups/VisionCameraCommandGroup.py | 10 -- .../CommandGroups/VisionCompensationGroup.py | 3 - .../UnitTest/TestVisionCameraCommandGroup.py | 78 ++++++++ .../UnitTest/TestVisionCommandGroup.py | 167 ++++++++++++++++++ .../TestVisionCompensationCommandGroup.py | 34 ++++ .../UnitTest/TestVisionPatternCommandGroup.py | 43 +++++ 6 files changed, 322 insertions(+), 13 deletions(-) create mode 100644 sentio_prober_control/UnitTest/TestVisionCameraCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestVisionCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestVisionCompensationCommandGroup.py create mode 100644 sentio_prober_control/UnitTest/TestVisionPatternCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/VisionCameraCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/VisionCameraCommandGroup.py index 48ec6af..6bef6cb 100644 --- a/sentio_prober_control/Sentio/CommandGroups/VisionCameraCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/VisionCameraCommandGroup.py @@ -15,7 +15,6 @@ class VisionCameraCommandGroup(CommandGroupBase): def __init__(self, comm) -> None: super().__init__(comm) - def set_light(self, mp: CameraMountPoint, value: int) -> None: """Set intensity of the light. @@ -27,7 +26,6 @@ def set_light(self, mp: CameraMountPoint, value: int) -> None: self.comm.send(f"vis:set_prop light, {mp.toSentioAbbr()}, {value}") Response.check_resp(self.comm.read_line()) - def set_exposure(self, mp: CameraMountPoint, value: int) -> None: """Set exposure time of the camera. @@ -42,7 +40,6 @@ def set_exposure(self, mp: CameraMountPoint, value: int) -> None: self.comm.send(f"vis:set_prop exposure, {mp.toSentioAbbr()}, {value}") Response.check_resp(self.comm.read_line()) - def set_gain(self, mp: CameraMountPoint, value: float) -> None: """Set gain of the camera. @@ -54,7 +51,6 @@ def set_gain(self, mp: CameraMountPoint, value: float) -> None: self.comm.send(f"vis:set_prop gain, {mp.toSentioAbbr()}, {value}") Response.check_resp(self.comm.read_line()) - def get_light(self, mp: CameraMountPoint) -> float: """Get light intensity. @@ -66,7 +62,6 @@ def get_light(self, mp: CameraMountPoint) -> float: resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) - def get_exposure(self, mp: CameraMountPoint) -> float: """Get exposure time. @@ -81,7 +76,6 @@ def get_exposure(self, mp: CameraMountPoint) -> float: resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) - def get_focus_value(self, mp: CameraMountPoint, alg: AutoFocusAlgorithm) -> float: """Get the focus value of the camera. @@ -101,7 +95,6 @@ def get_focus_value(self, mp: CameraMountPoint, alg: AutoFocusAlgorithm) -> floa resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) - def get_gain(self, mp: CameraMountPoint) -> float: """Get gain of the camera. @@ -116,7 +109,6 @@ def get_gain(self, mp: CameraMountPoint) -> float: resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) - def get_calib(self, mp: CameraMountPoint) -> Tuple[float, float]: """Get the calibration data of the camera. @@ -136,7 +128,6 @@ def get_calib(self, mp: CameraMountPoint) -> Tuple[float, float]: tok = resp.message().split(",") return float(tok[0]), float(tok[1]) - def get_image_size(self, mp: CameraMountPoint) -> Tuple[int, int]: """Get size of the image. @@ -153,7 +144,6 @@ def get_image_size(self, mp: CameraMountPoint) -> Tuple[int, int]: tok = resp.message().split(",") return int(tok[0]), int(tok[1]) - def is_pattern_trained(self, mp: CameraMountPoint, pat) -> bool: """Check if a pattern is trained. diff --git a/sentio_prober_control/Sentio/CommandGroups/VisionCompensationGroup.py b/sentio_prober_control/Sentio/CommandGroups/VisionCompensationGroup.py index bb39902..1c791ab 100644 --- a/sentio_prober_control/Sentio/CommandGroups/VisionCompensationGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/VisionCompensationGroup.py @@ -17,7 +17,6 @@ class VisionCompensationGroup(CommandGroupBase): def __init__(self, comm): super().__init__(comm) - @deprecated("Use vision.compensation.enable() instead") def set_compensation(self, comp: CompensationMode, enable: bool) -> Tuple[str, str]: self.comm.send(f"vis:compensation:enable {comp.toSentioAbbr()}, {enable}") @@ -25,7 +24,6 @@ def set_compensation(self, comp: CompensationMode, enable: bool) -> Tuple[str, s tok = resp.message().split(",") return tok[0], tok[1] - def enable(self, comp: CompensationMode, enable: bool) -> Tuple[str, str]: """Enable or disable compensation for a given subsystem. @@ -45,7 +43,6 @@ def enable(self, comp: CompensationMode, enable: bool) -> Tuple[str, str]: tok = resp.message().split(",") return tok[0], tok[1] - def start_execute(self, type: CompensationType, mode: CompensationMode) -> Response: """Start the execution of a compensation. diff --git a/sentio_prober_control/UnitTest/TestVisionCameraCommandGroup.py b/sentio_prober_control/UnitTest/TestVisionCameraCommandGroup.py new file mode 100644 index 0000000..e2b53f3 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestVisionCameraCommandGroup.py @@ -0,0 +1,78 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.Enumerations import CameraMountPoint, AutoFocusAlgorithm + + +class TestVisionCameraCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock() + self.prober = SentioProber(self.mock_comm) + + def test_set_light(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.camera.set_light(CameraMountPoint.Scope, 80) + self.mock_comm.send.assert_called_with("vis:set_prop light, scope, 80") + + def test_set_exposure(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.camera.set_exposure(CameraMountPoint.Scope, 2000) + self.mock_comm.send.assert_called_with("vis:set_prop exposure, scope, 2000") + + def test_set_gain(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.camera.set_gain(CameraMountPoint.Scope, 1.5) + self.mock_comm.send.assert_called_with("vis:set_prop gain, scope, 1.5") + + def test_get_light(self): + self.mock_comm.read_line.return_value = "0,0,90" + result = self.prober.vision.camera.get_light(CameraMountPoint.Scope) + self.mock_comm.send.assert_called_with("vis:get_prop light, scope") + self.assertEqual(result, 90.0) + + def test_get_exposure(self): + self.mock_comm.read_line.return_value = "0,0,2500" + result = self.prober.vision.camera.get_exposure(CameraMountPoint.Scope) + self.mock_comm.send.assert_called_with("vis:get_prop exposure, scope") + self.assertEqual(result, 2500.0) + + def test_get_focus_value(self): + self.mock_comm.read_line.return_value = "0,0,35" + result = self.prober.vision.camera.get_focus_value(CameraMountPoint.Scope, AutoFocusAlgorithm.Bandpass) + self.mock_comm.send.assert_called_with("vis:get_focus_value scope, Bandpass") + self.assertEqual(result, 35.0) + + def test_get_gain(self): + self.mock_comm.read_line.return_value = "0,0,2.3" + result = self.prober.vision.camera.get_gain(CameraMountPoint.Scope) + self.mock_comm.send.assert_called_with("vis:get_prop gain, scope") + self.assertEqual(result, 2.3) + + def test_get_calib(self): + self.mock_comm.read_line.return_value = "0,0,0.12,0.15" + result = self.prober.vision.camera.get_calib(CameraMountPoint.Scope) + self.mock_comm.send.assert_called_with("vis:get_prop calib, scope") + self.assertEqual(result, (0.12, 0.15)) + + def test_get_image_size(self): + self.mock_comm.read_line.return_value = "0,0,1920,1080" + result = self.prober.vision.camera.get_image_size(CameraMountPoint.Scope) + self.mock_comm.send.assert_called_with("vis:get_prop image_size, scope") + self.assertEqual(result, (1920, 1080)) + + def test_is_pattern_trained_true(self): + self.mock_comm.read_line.return_value = "0,0,1" + result = self.prober.vision.camera.is_pattern_trained(CameraMountPoint.Scope, "MyPattern") + self.mock_comm.send.assert_called_with("vis:pattern:is_trained scope, MyPattern") + self.assertTrue(result) + + def test_is_pattern_trained_false(self): + self.mock_comm.read_line.return_value = "0,0,0" + result = self.prober.vision.camera.is_pattern_trained(CameraMountPoint.Scope, "NotTrained") + self.mock_comm.send.assert_called_with("vis:pattern:is_trained scope, NotTrained") + self.assertFalse(result) + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestVisionCommandGroup.py b/sentio_prober_control/UnitTest/TestVisionCommandGroup.py new file mode 100644 index 0000000..5e86862 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestVisionCommandGroup.py @@ -0,0 +1,167 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.Enumerations import ( + CameraMountPoint, + AutoFocusCmd, + AutoAlignCmd, + PtpaFindTipsMode, + SnapshotType, + SnapshotLocation, + PtpaType, DieCompensationType, DieCompensationMode, MoveAxis +) + + +class TestVisionCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.prober = SentioProber(self.mock_comm) + + def test_align_wafer(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.align_wafer(AutoAlignCmd.UpdateDieSize) + self.mock_comm.send.assert_called_with("vis:align_wafer update") + + def test_align_die(self): + self.mock_comm.read_line.return_value = "0,0,10.0,20.0,0.1" + result = self.prober.vision.align_die() + self.mock_comm.send.assert_called_with("vis:align_die 0.05") + self.assertEqual(result, (10.0, 20.0, 0.1)) + + def test_auto_focus_default(self): + self.mock_comm.read_line.return_value = "0,0,13500,scope" + result = self.prober.vision.auto_focus() + self.mock_comm.send.assert_called_with("vis:auto_focus F") + self.assertEqual(result, (13500.0, MoveAxis.Scope)) + + def test_auto_focus_parameter(self): + self.mock_comm.read_line.return_value = "0,0,46500,chuck" + result = self.prober.vision.auto_focus(AutoFocusCmd.GoTo) + self.mock_comm.send.assert_called_with("vis:auto_focus G") + self.assertEqual(result, (46500.0, MoveAxis.Chuck)) + + def test_camera_synchronize(self): + self.mock_comm.read_line.return_value = "0,0,1.1,2.2,3.3" + result = self.prober.vision.camera_synchronize() + self.mock_comm.send.assert_called_with("vis:camera_synchronize") + self.assertEqual(result, (1.1, 2.2, 3.3)) + + def test_find_home(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.find_home() + self.mock_comm.send.assert_called_with("vis:find_home") + + def test_enable_follow_mode(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.enable_follow_mode(True) + self.mock_comm.send.assert_called_with("vis:enable_follow_mode True") + + def test_switch_all_lights(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.switch_all_lights(True) + self.mock_comm.send.assert_called_with("vis:switch_all_lights True") + + def test_detect_probetips(self): + self.mock_comm.read_line.return_value = "0,0,100 200 10 10 0.98 1" + result = self.prober.vision.detect_probetips(CameraMountPoint.Scope) + self.mock_comm.send.assert_called_with("vis:detect_probetips scope, ProbeDetector, Roi") + self.assertEqual(result[0][0:5], [100.0, 200.0, 10.0, 10.0, 0.98]) + + def test_ptpa_find_tips(self): + self.mock_comm.read_line.return_value = "0,0,100.0,200.0,300.0" + result = self.prober.vision.ptpa_find_tips(PtpaFindTipsMode.OnAxis) + self.mock_comm.send.assert_called_with("vis:ptpa_find_tips OnAxis") + self.assertEqual(result, (100.0, 200.0, 300.0)) + + def test_snap_image_to_prober(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.snap_image("test.jpg", SnapshotType.CameraRaw, SnapshotLocation.Prober) + self.mock_comm.send.assert_called_with("vis:snap_image test.jpg, 0") + + def test_get_light_status(self): + self.mock_comm.read_line.return_value = "0,0,1" + result = self.prober.vision.get_light_status("scope") + self.mock_comm.send.assert_called_with("vis:get_light_status scope") + self.assertTrue(result) + + def test_get_lens_zoom_level(self): + self.mock_comm.read_line.return_value = "0,0,8.0" + result = self.prober.vision.get_lens_zoom_level() + self.mock_comm.send.assert_called_with("vis:get_lens_zoom_level") + self.assertEqual(result, 8.0) + + def test_set_lens_zoom_level(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.set_lens_zoom_level(6.0) + self.mock_comm.send.assert_called_with("vis:set_lens_zoom_level 6.0") + + def test_find_thermal_die_size(self): + self.mock_comm.read_line.return_value = "0,0,1.001,0.999" + result = self.prober.vision.find_thermal_die_size() + self.mock_comm.send.assert_called_with("vis:find_thermal_die_size") + self.assertEqual(result, (1.001, 0.999)) + + def test_find_pattern(self): + self.mock_comm.read_line.return_value = "0,0,95.0,100.0,200.0,0.3" + result = self.prober.vision.find_pattern("MyPattern", 95, 1) + self.mock_comm.send.assert_called_with("vis:find_pattern MyPattern, 95, 1, CenterOfRoi") + self.assertEqual(result, (95.0, 100.0, 200.0, 0.3)) + + def test_has_camera_true(self): + self.mock_comm.read_line.return_value = "0,0,1" + result = self.prober.vision.has_camera(CameraMountPoint.Scope) + self.mock_comm.send.assert_called_with("vis:has_camera scope") + self.assertTrue(result) + + def test_has_camera_false(self): + self.mock_comm.read_line.return_value = "0,0,0" + result = self.prober.vision.has_camera(CameraMountPoint.OffAxis) + self.mock_comm.send.assert_called_with("vis:has_camera offaxis") + self.assertFalse(result) + + def test_remove_probetip_marker(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.remove_probetip_marker() + self.mock_comm.send.assert_called_with("vis:remove_probetip_marker") + + def test_match_tips(self): + self.mock_comm.read_line.return_value = "0,0,0.123,0.456" + result = self.prober.vision.match_tips(PtpaType.OffAxis) + self.mock_comm.send.assert_called_with("vis:match_tips offaxis") + self.assertEqual(result, (0.123, 0.456)) + + def test_switch_light(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.switch_light(CameraMountPoint.Scope, True) + self.mock_comm.send.assert_called_with("vis:switch_light scope, True") + + def test_switch_camera(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.switch_camera(CameraMountPoint.OffAxis) + self.mock_comm.send.assert_called_with("vis:switch_camera offaxis") + + def test_ptpa_find_pads(self): + self.mock_comm.read_line.return_value = "0,0,10.0,20.0,30.0" + result = self.prober.vision.ptpa_find_pads(5, 10) + self.mock_comm.send.assert_called_with("vis:execute_ptpa_find_pads 5,10") + self.assertEqual(result, (10.0, 20.0, 30.0)) + + def test_start_fast_track(self): + self.prober.send_cmd = MagicMock() + self.prober.vision.start_fast_track() + self.prober.send_cmd.assert_called_with("vis:start_fast_track") + + def test_start_execute_compensation(self): + self.mock_comm.read_line.return_value = "0,3,OK" + result = self.prober.vision.start_execute_compensation( + DieCompensationType.DieAlign, + DieCompensationMode.Lateral + ) + self.mock_comm.send.assert_called_with("vis:compensation:start_execute DieAlign,Lateral") + self.assertIsInstance(result.cmd_id(), int) # cmd_id should be returned + self.assertGreater(result.cmd_id(), 0, "cmd_id should be greater than 0") + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestVisionCompensationCommandGroup.py b/sentio_prober_control/UnitTest/TestVisionCompensationCommandGroup.py new file mode 100644 index 0000000..de199ec --- /dev/null +++ b/sentio_prober_control/UnitTest/TestVisionCompensationCommandGroup.py @@ -0,0 +1,34 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Sentio.Enumerations import CompensationMode, CompensationType + + +class TestVisionCompensationGroup(unittest.TestCase): + + def setUp(self): + self.mock_comm = MagicMock() + self.prober = SentioProber(self.mock_comm) + + def test_enable_compensation(self): + self.mock_comm.read_line.return_value = "0,0,Lateral,Vertical" + result = self.prober.vision.compensation.enable(CompensationMode.Lateral, True) + self.mock_comm.send.assert_called_with("vis:compensation:enable Lateral, True") + self.assertEqual(result, ("Lateral", "Vertical")) + + def test_set_compensation_deprecated(self): + self.mock_comm.read_line.return_value = "0,0,Off,On" + result = self.prober.vision.compensation.set_compensation(CompensationMode.Vertical, False) + self.mock_comm.send.assert_called_with("vis:compensation:enable Vertical, False") + self.assertEqual(result, ("Off", "On")) + + def test_start_execute_compensation(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.prober.vision.compensation.start_execute(CompensationType.Topography, CompensationMode.Vertical) + self.mock_comm.send.assert_called_with("vis:compensation:start_execute Topography, Vertical") + self.assertTrue(resp.ok()) + self.assertEqual(resp.message(), "OK") + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestVisionPatternCommandGroup.py b/sentio_prober_control/UnitTest/TestVisionPatternCommandGroup.py new file mode 100644 index 0000000..8ce71f2 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestVisionPatternCommandGroup.py @@ -0,0 +1,43 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Sentio.Enumerations import CameraMountPoint, FindPatternReference, DefaultPattern +from sentio_prober_control.Sentio.ProberSentio import SentioProber + + +class TestPatternCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock() + self.prober = SentioProber(self.mock_comm) + + def test_find_pattern(self): + self.mock_comm.read_line.return_value = "0,0,85.2,120.0,-30.5,1.0" + result = self.prober.vision.pattern.find("MyPattern", threshold=85, pattern_index=1, + reference=FindPatternReference.CenterOfRoi) + self.mock_comm.send.assert_called_with("vis:find_pattern MyPattern, 85, 1, CenterOfRoi") + self.assertEqual(result, (85.2, 120.0, -30.5, 1.0)) + + def test_get_chuck_pos(self): + self.mock_comm.read_line.return_value = "0,0,100.0,200.0" + result = self.prober.vision.pattern.get_chuck_pos(CameraMountPoint.Scope, DefaultPattern.DieAlignPos1) + self.mock_comm.send.assert_called_with("vis:pattern:get_chuck_pos scope, diealignpos1") + self.assertEqual(result, (100.0, 200.0)) + + def test_set_chuck_pos(self): + self.mock_comm.read_line.return_value = "0,0,100.0,200.0" + result = self.prober.vision.pattern.set_chuck_pos(CameraMountPoint.Scope, DefaultPattern.TwoPoint, 100.0, 200.0) + self.mock_comm.send.assert_called_with("vis:pattern:set_chuck_pos scope, 2pt, 100.0, 200.0") + self.assertEqual(result, (100.0, 200.0)) + + def test_show_training_box(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.pattern.show_training_box(True) + self.mock_comm.send.assert_called_with("vis:pattern:show_training_box true") + + def test_train(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.prober.vision.pattern.train("MyPattern") + self.mock_comm.send.assert_called_with("vis:pattern:train MyPattern") + + +if __name__ == "__main__": + unittest.main() From c94252c5c28ae81d0e342825c38100ec6d811367 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 10 Apr 2025 15:44:44 +0800 Subject: [PATCH 17/41] add John's remote command. --- .../Sentio/CommandGroups/SetupCommandGroup.py | 69 + .../CommandGroups/StatusCommandGroup.py | 101 +- sentio_prober_control/Sentio/ProberSentio.py | 198 ++- .../Sentio/ProberSentio.py.bak | 1508 +++++++++++++++++ .../UnitTest/TestProberSentio.py | 286 +--- .../UnitTest/TestSetupCommandGroup.py | 42 + .../UnitTest/TestStatusCommandGroup.py | 206 ++- 7 files changed, 2086 insertions(+), 324 deletions(-) create mode 100644 sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py create mode 100644 sentio_prober_control/Sentio/ProberSentio.py.bak create mode 100644 sentio_prober_control/UnitTest/TestSetupCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py new file mode 100644 index 0000000..6c6c90b --- /dev/null +++ b/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py @@ -0,0 +1,69 @@ +from typing import Tuple + +from sentio_prober_control.Sentio.Enumerations import ThermoChuckState +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ( + ModuleCommandGroupBase, +) + + +class SetupCommandGroup(ModuleCommandGroupBase): + """A command group for accessing setup module functions.""" + def __init__(self, comm) -> None: + super().__init__(comm, "setup") + + def get_contact_counter(self) -> int: + """Retrieves the contact counter value. + + Returns: + An integer representing the number of times the chuck moved into contact height, + excluding moves on cleaning substrate. + """ + self.comm.send("setup:contact_counter:get") + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) + + def reset_contact_counter(self) -> None: + """Resets the contact counter.""" + self.comm.send("setup:contact_counter:reset") + Response.check_resp(self.comm.read_line()) + + def remote_light_off_at_contact(self, status: bool) -> str: + """Defines whether light is switched off at contact height in remote mode. + + Args: + status (BooleanStatus): True/False, ON/OFF as defined in Enum. + + Returns: + Response: Response object for result checking. + """ + self.comm.send(f"setup:remote:light_off_at_contact {status}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def remote_light_on_at_separation(self, status: bool) -> str: + """Defines whether light is switched on at separation height in remote mode. + + Args: + status (BooleanStatus): True/False, ON/OFF as defined in Enum. + + Returns: + Response: Response object for result checking. + """ + self.comm.send(f"setup:remote:light_on_at_separation {status}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def remote_scope_follow_off(self, status: bool) -> str: + """Defines whether scope follow mode is switched off in remote mode. + + Args: + status (BooleanStatus): True/False, ON/OFF as defined in Enum. + Note: ON disables scope follow. + + Returns: + Response: Response object for result checking. + """ + self.comm.send(f"setup:remote:scope_follow_off {status}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 9aefd49..7cfc09c 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -10,7 +10,6 @@ ModuleCommandGroupBase, ) - class StatusCommandGroup(ModuleCommandGroupBase): """A command group for getting the status of the probe station and controlling the dashboard module.""" @@ -218,4 +217,102 @@ def set_high_purge(self, enable: Union[bool, HighPurgeState, str]) -> Response: elif isinstance(enable, str): enable = HighPurgeState[enable] self.comm.send(f"status:set_high_purge {enable.toSentioAbbr()}") - return Response.check_resp(self.comm.read_line()) \ No newline at end of file + return Response.check_resp(self.comm.read_line()) + + def get_access_level(self) -> str: + """Retrieves the access level of operation. + + Returns: + A string representing the access level. Possible values are: + 'Operator', 'Admin', 'Service', 'Engineer', 'Debug'. + """ + self.comm.send("status:get_access_level") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def get_prop(self, prop_name: str, stage: str = None) -> str: + """Retrieve a specific data item from the setup module. + + Args: + prop_name: The property name to query. Valid examples include: + 'Edge_Sensor', 'Z_Position_Hint', 'Active_Stage'. + stage: Optional stage name for 'Z_Position_Hint'. Can be 'Chuck', 'Scope', 'Probe01' ~ 'Probe04'. + + Returns: + A string value representing the queried property. + """ + cmd = f"status:get_prop {prop_name}" + if stage: + cmd += f" {stage}" + self.comm.send(cmd) + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def get_machine_id(self) -> str: + """Retrieves the machine ID. + + Returns: + A string containing the machine ID. + """ + self.comm.send("status:get_machine_id") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def get_version(self) -> str: + """Retrieves the system version. + + Returns: + A string containing version information. + """ + self.comm.send("status:get_version") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def set_prop(self, prop_name: str, stage: str, chuck_site: str = None) -> Response: + """Set a specific property in the dashboard module. + + Args: + prop_name: The property name (e.g., 'Active_Stage'). + stage: The stage to apply the setting (e.g., 'Chuck', 'Scope'). + chuck_site: Optional. Only used when stage is 'Chuck' (e.g., 'Wafer', 'Auxright1'). + + Returns: + A Response object after setting the property. + """ + cmd = f"status:set_prop {prop_name} {stage}" + if chuck_site: + cmd += f", {chuck_site}" + self.comm.send(cmd) + return Response.check_resp(self.comm.read_line()) + + def show_message(self, message: str, button: str = "OK", caption: str = "None", level: str = "Hint") -> str: + """Show a message dialog for user interaction. + + Args: + message: The message to show. + button: The button configuration (e.g., 'OK', 'OKCancel', 'YesNo'). + caption: Caption for the dialog. + level: Level of the message (e.g., 'Hint', 'Warning', 'Error'). + + Returns: + The button that was pressed. + """ + self.comm.send(f"status:show_message {message},{button},{caption},{level}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def start_show_message(self, message: str, button: str = "OK", caption: str = "None", level: str = "Hint") -> str: + """Start an asynchronous message dialog. + + Args: + message: The message to show. + button: The button configuration. + caption: The dialog caption. + level: The level of message importance. + + Returns: + A string containing Command ID and button pressed info. + """ + self.comm.send(f"status:start_show_message {message},{button},{caption},{level}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() \ No newline at end of file diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 8904da9..fdbc4c8 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -44,8 +44,7 @@ from sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup import StatusCommandGroup from sentio_prober_control.Sentio.CommandGroups.VisionCommandGroup import VisionCommandGroup from sentio_prober_control.Sentio.CommandGroups.WafermapCommandGroup import WafermapCommandGroup -from sentio_prober_control.Sentio.Enumerations import ChuckSite - +from sentio_prober_control.Sentio.CommandGroups.SetupCommandGroup import SetupCommandGroup class SentioCommunicationType(Enum): """This enum defines different types of prober communication. @@ -101,6 +100,7 @@ def __init__(self, comm: CommunicatorBase): self.siph: SiPHCommandGroup = SiPHCommandGroup(self) self.status: StatusCommandGroup = StatusCommandGroup(self) self.vision: VisionCommandGroup = VisionCommandGroup(self) + self.setup: SetupCommandGroup = SetupCommandGroup(self) def abort_command(self, cmd_id: int) -> Response: @@ -116,7 +116,7 @@ def abort_command(self, cmd_id: int) -> Response: return Response.check_resp(self.comm.read_line()) - def clear_contact(self, site: ChuckSite | None = None) -> None: + def clear_contact(self, site: ChuckSite | None = None) -> Response: """Clear contact positions. Args: @@ -131,7 +131,7 @@ def clear_contact(self, site: ChuckSite | None = None) -> None: else: self.comm.send(f"clear_contact {site.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) @staticmethod @@ -156,7 +156,7 @@ def create_prober(comm_type : str | SentioCommunicationType, arg1 : str | GpibCa raise ValueError(f'Unknown prober type: "{comm_type}"') - def enable_chuck_overtravel(self, stat: bool) -> None: + def enable_chuck_overtravel(self, stat: bool) -> Response: """Enable chuck overtravel. This function wraps SENTIO's "enable_chuck_overtravel" remote command. @@ -169,10 +169,10 @@ def enable_chuck_overtravel(self, stat: bool) -> None: """ self.comm.send(f"enable_chuck_overtravel {stat}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) - def enable_chuck_hover(self, stat: bool) -> None: + def enable_chuck_hover(self, stat: bool) -> Response: """Enable chuck hover height. The Hover height is a height that is significantly closer to the chuck compared to the separation height. @@ -189,10 +189,10 @@ def enable_chuck_hover(self, stat: bool) -> None: """ self.comm.send(f"enable_chuck_hover {stat}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) - def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> None: + def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> Response: """Enable chuck site hover height. The Hover height is a height that is significantly closer to the chuck compared to the separation height. @@ -208,10 +208,10 @@ def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> None: """ self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) - def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> None: + def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> Response: """Enable overtravel distance for a specific chuck site. Args: @@ -223,7 +223,7 @@ def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> None: """ self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) def file_transfer(self, source: str, dest: str) -> Response: @@ -557,7 +557,7 @@ def move_chuck_home(self) -> Tuple[float, float]: return float(tok[0]), float(tok[1]) - def move_chuck_load(self, pos: LoadPosition) -> None: + def move_chuck_load(self, pos: LoadPosition) -> Response: """Move chuck to load position. Wraps SENTIO's "move_chuck_load" remote command. @@ -569,7 +569,7 @@ def move_chuck_load(self, pos: LoadPosition) -> None: A response object with the result of the command. """ self.comm.send(f"move_chuck_load {pos.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) def move_chuck_separation(self) -> float: @@ -650,7 +650,7 @@ def move_chuck_z(self, ref: ChuckZReference, z: float) -> float: resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) - def move_chuck_work_area(self, area: WorkArea) -> None: + def move_chuck_work_area(self, area: WorkArea) -> Response: """Move the chuck to a given work area. A SENTIO probe station can have different work areas. One area is for probing. This is the default @@ -669,7 +669,7 @@ def move_chuck_work_area(self, area: WorkArea) -> None: A response object with the result of the command. """ self.comm.send(f"move_chuck_work_area {area.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) def move_scope_xy( self, ref: ScopeXYReference, x: float, y: float @@ -692,7 +692,7 @@ def move_scope_xy( tok = resp.message().split(",") return float(tok[0]), float(tok[1]) - def move_scope_lift(self, state: bool) -> None: + def move_scope_lift(self, state: bool): """Move scope to its lift position. The scope lift position is a position where the scope is @@ -910,7 +910,7 @@ def set_stepping_contact_mode(self, mode: SteppingContactMode) -> None: Response.check_resp(self.comm.read_line()) - def set_vacuum(self, site: ChuckSite, stat: bool) -> None: + def set_vacuum(self, site: ChuckSite, stat: bool) -> Response: """Switches the vacuum of a chuck site on or off. Args: @@ -922,7 +922,7 @@ def set_vacuum(self, site: ChuckSite, stat: bool) -> None: """ self.comm.send(f"set_vacuum {site.toSentioAbbr()}, {stat}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) def show_hint(self, msg: str, subtext: str) -> Response: @@ -1373,8 +1373,6 @@ def set_chuck_separation_gap(self, gap: float) -> None: self.comm.send(f"set_chuck_separation_gap {gap}") Response.check_resp(self.comm.read_line()) - from sentio_prober_control.Sentio.Enumerations import ChuckSite - from sentio_prober_control.Sentio.Response import Response def set_chuck_site_overtravel_gap(self, site: ChuckSite, gap: float) -> None: """Set overtravel gap for a specific chuck site. @@ -1506,3 +1504,161 @@ def create_project(self, path_or_name: str) -> str: full_path = tok[0] return full_path + + def get_indexer_pos(self) -> Tuple[int, str]: + """Get currently indexer position. + + Returns: + Tuple[int, str]: Indexer location (1~5), and indexer z-position ("up" or "down"). + """ + self.comm.send("get_indexer_pos") + resp = Response.check_resp(self.comm.read_line()) + location, position = resp.message().split(",") + return int(location), position + + def indexer_cda(self, on: bool) -> Response: + """Turn indexer CDA on or off. + + Args: + on (bool): True to turn on, False to turn off. + + Returns: + Response: Result response. + """ + state = "on" if on else "off" + self.comm.send(f"indexer_cda {state}") + return Response.check_resp(self.comm.read_line()) + + def move_bottom_platen_contact(self) -> float: + """Move bottom platen to contact height. + + Returns: + float: New Z position in µm with respect to zero. + """ + self.comm.send("move_bottom_platen_contact") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + def move_bottom_platen_separation(self) -> float: + """Move bottom platen to separation height. + + Returns: + float: New Z position in µm with respect to zero. + """ + self.comm.send("move_bottom_platen_separation") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + def move_indexer_lift(self) -> Response: + """Move indexer up. + + Returns: + Response: Result response. + """ + self.comm.send("move_indexer_lift") + return Response.check_resp(self.comm.read_line()) + + def move_indexer_down(self) -> Response: + """Move indexer down. + + Returns: + Response: Result response. + """ + self.comm.send("move_indexer_down") + return Response.check_resp(self.comm.read_line()) + + def probe_air_lift(self, valve: str, position: str) -> Response: + """Control probe air lift mechanism. + + Args: + valve (str): v1/v2 + position (str): lift or unlift + + Returns: + Response: Result response. + """ + self.comm.send(f"probe_air_lift {valve},{position}") + return Response.check_resp(self.comm.read_line()) + + def set_signal_tower(self, red: int, yellow: int, green: int, blue: int) -> Response: + """Set signal tower LED states. + + Args: + red (int): -1=no change, 0=off, 1=on, 2=blinking + yellow (int) + green (int) + blue (int) + + Returns: + Response: Result response. + """ + self.comm.send(f"set_signal_tower {red},{yellow},{green},{blue}") + return Response.check_resp(self.comm.read_line()) + + def set_signal_tower_buzzer(self, state: int) -> Response: + """Set signal tower buzzer state. + + Args: + state (int): -1=no change, 0=off, 1=on, 2=pulsed + + Returns: + Response: Result response. + """ + self.comm.send(f"set_signal_tower_buzzer {state}") + return Response.check_resp(self.comm.read_line()) + + def start_move_indexer_pos(self, pos: int) -> Response: + """Asynchronously move indexer to position. + + Args: + pos (int): Position index from 1 to 5 + + Returns: + Response: Includes cmd_id for async tracking. + """ + self.comm.send(f"start_move_indexer_pos {pos}") + return Response.check_resp(self.comm.read_line()) + + def swap_bridge(self, side: str, device_position: str = None) -> Response: + """Control swap bridge side and position. + + Args: + side (str): right/left/current + device_position (str, optional): up/down + + Returns: + Response: Result response. + """ + cmd = f"swap_bridge {side}" + if device_position: + cmd += f",{device_position}" + self.comm.send(cmd) + return Response.check_resp(self.comm.read_line()) + + def get_door_status(self, door: str) -> Tuple[bool, bool]: + """Retrieves the closed and locked state of a door. + + Args: + door (str): The door to query ("prober" or "loader"). + + Returns: + Tuple[bool, bool]: (is_closed, is_locked) + """ + self.comm.send(f"get_door_status {door.lower()}") + resp = Response.check_resp(self.comm.read_line()) + closed, locked = resp.message().split(",") + return closed == "1", locked == "1" + + def set_door_lock(self, door: str, lock: bool) -> Response: + """Locks or unlocks the specified door. + + Args: + door (str): The door to control ("prober" or "loader"). + lock (bool): True to lock the door, False to unlock. + + Returns: + Response: Result of the operation. + """ + state = "1" if lock else "0" + self.comm.send(f"set_door_lock {door.lower()},{state}") + return Response.check_resp(self.comm.read_line()) diff --git a/sentio_prober_control/Sentio/ProberSentio.py.bak b/sentio_prober_control/Sentio/ProberSentio.py.bak new file mode 100644 index 0000000..8904da9 --- /dev/null +++ b/sentio_prober_control/Sentio/ProberSentio.py.bak @@ -0,0 +1,1508 @@ +import base64 +import os +from typing import Tuple +from enum import Enum + +from deprecated import deprecated + +from sentio_prober_control.Sentio.Enumerations import ( + ChuckSite, + ChuckXYReference, + ChuckThetaReference, + ChuckZReference, + DialogButtons, + LoadPosition, + ProjectFileInfo, + Module, + ScopeXYReference, + ScopeZReference, + Stage, + SteppingContactMode, + VceZReference, + WorkArea, + ChuckPositionHint, + ChuckSpeed, + VacuumState, + HighPowerAirState, + SoftContactState, + UserCoordState +) + +from sentio_prober_control.Sentio.ProberBase import ProberBase, ProberException +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Communication.CommunicatorBase import CommunicatorBase +from sentio_prober_control.Communication.CommunicatorGpib import CommunicatorGpib, GpibCardVendor +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Communication.CommunicatorVisa import CommunicatorVisa +from sentio_prober_control.Sentio.CommandGroups.AuxCommandGroup import AuxCommandGroup +from sentio_prober_control.Sentio.CommandGroups.CompensationCommandGroup import CompensationCommandGroup +from sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup import LoaderCommandGroup +from sentio_prober_control.Sentio.CommandGroups.ProbeCommandGroup import ProbeCommandGroup +from sentio_prober_control.Sentio.CommandGroups.QAlibriaCommandGroup import QAlibriaCommandGroup +from sentio_prober_control.Sentio.CommandGroups.ServiceCommandGroup import ServiceCommandGroup +from sentio_prober_control.Sentio.CommandGroups.SiPHCommandGroup import SiPHCommandGroup +from sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup import StatusCommandGroup +from sentio_prober_control.Sentio.CommandGroups.VisionCommandGroup import VisionCommandGroup +from sentio_prober_control.Sentio.CommandGroups.WafermapCommandGroup import WafermapCommandGroup +from sentio_prober_control.Sentio.Enumerations import ChuckSite + + +class SentioCommunicationType(Enum): + """This enum defines different types of prober communication. + + Attributes: + BackToContact (0): Chuck will move back to contact position after stepping. + StepToSeparation (1): Chuck will move to separation position after stepping. + LockContact (2): Chuck cannot step when at contact. You will have to manually move it away from its contact position before issuing the next step command. + """ + + TcpIp = 0 + Gpib = 1 + Visa = 2 + + +class SentioProber(ProberBase): + """This class represents the SENTIO probe station in python. + It provides wrapper for most of the remote commands exposed by SENTIO. + + Attributes: + aux (AuxCommandGroup): The aux command group provides access the the aux site modules functionality. + loader (LoaderCommandGroup): The loader command group provides access to the loader modules functionality. + map (WafermapCommandGroup): The wafermap command group provides access to the wafermap modules functionality. + probe (ProbeCommandGroup): The probe command group provides access to the probe modules functionality. + qalibria (QAlibriaCommandGroup): The qalibria command group provides access to the qalibria modules functionality. + service (ServiceCommandGroup): The service command group provides access to the service modules functionality. + siph (SiPHCommandGroup): The siph command group provides access to the SiPH modules functionality. + status (StatusCommandGroup): The status command group provides access to the dashboard modules functionality. (formerly called status module) + vision (VisionCommandGroup): The vision command group provides access to the vision modules functionality. + """ + + def __init__(self, comm: CommunicatorBase): + """Construct a SENTIO prober object. + + The prober must be initialized with a communication object that + specifies how the system communicates with the probe station. + + Args: + comm (CommunicatorBase): The communicator to use for communication with the prober. + """ + ProberBase.__init__(self, comm) + + self.__name = "SentioProber" + self.comm.send("*RCS 1") # switch to the native SENTIO remote command set + + self.aux: AuxCommandGroup = AuxCommandGroup(self) + self.compensation: CompensationCommandGroup = CompensationCommandGroup(self) + self.loader: LoaderCommandGroup = LoaderCommandGroup(self) + self.map: WafermapCommandGroup = WafermapCommandGroup(self) + self.probe: ProbeCommandGroup = ProbeCommandGroup(self) + self.qalibria: QAlibriaCommandGroup = QAlibriaCommandGroup(self) + self.service: ServiceCommandGroup = ServiceCommandGroup(self) + self.siph: SiPHCommandGroup = SiPHCommandGroup(self) + self.status: StatusCommandGroup = StatusCommandGroup(self) + self.vision: VisionCommandGroup = VisionCommandGroup(self) + + + def abort_command(self, cmd_id: int) -> Response: + """Stop an ongoing asynchronous remote command. + + Args: + cmd_id (int): The id of the async command to abort. + + Returns: + A response object with the result of the command. + """ + self.comm.send("abort_command {0}".format(cmd_id)) + return Response.check_resp(self.comm.read_line()) + + + def clear_contact(self, site: ChuckSite | None = None) -> None: + """Clear contact positions. + + Args: + site (ChuckSite): The chuck site to clear. If None is given all sites will be cleared. + + Returns: + A response object with the result of the command. + """ + + if site is None: + self.comm.send("clear_contact") + else: + self.comm.send(f"clear_contact {site.toSentioAbbr()}") + + Response.check_resp(self.comm.read_line()) + + + @staticmethod + def create_prober(comm_type : str | SentioCommunicationType, arg1 : str | GpibCardVendor = "127.0.0.1:35555", arg2 : str = "") -> 'SentioProber': + """ Create an instance of a SentioProber object that is bound to a certain communication method. Your choices of communication are tcpip, gpib and visa. + + Args: + comm_type (str): The type of communication to use. Valid values are "tcpip", "gpib" and "visa". + arg1 (object): For tcpip this is a single string specifying address and port + like "127.0.0.1:35555". For visa this is the address of the device like "GPIB0::20::INSTR". For gpib two parameters are needed. + The first one is the [type of driver/card](/Communication/GpibCardVendor) installed in the system, the second parameter is the address of the device like "GPIB0:20". + arg2 (str): Only used for gpib communication. This is the GPIB address of the prober i.e. "GPIB0:20". + """ + + if comm_type == "tcpip" or comm_type == SentioCommunicationType.TcpIp: + return SentioProber(CommunicatorTcpIp.create(arg1)) + elif comm_type == "gpib" or comm_type == SentioCommunicationType.Gpib: + return SentioProber(CommunicatorGpib.create(arg1, arg2)) + elif comm_type == "visa" or comm_type == SentioCommunicationType.Visa: + return SentioProber(CommunicatorVisa.create(arg1)) + else: + raise ValueError(f'Unknown prober type: "{comm_type}"') + + + def enable_chuck_overtravel(self, stat: bool) -> None: + """Enable chuck overtravel. + + This function wraps SENTIO's "enable_chuck_overtravel" remote command. + + Args: + stat (bool): True to enable, False to disable. + + Returns: + A response object with the result of the command. + """ + + self.comm.send(f"enable_chuck_overtravel {stat}") + Response.check_resp(self.comm.read_line()) + + + def enable_chuck_hover(self, stat: bool) -> None: + """Enable chuck hover height. + + The Hover height is a height that is significantly closer to the chuck compared to the separation height. + It is closer to the wafer but it is not safe for fast or long chuck moves as the chuck may be slightly + tilted. + + This function wraps SENTIO's "enable_chuck_hover" remote command. + + Args: + stat (bool): True to enable, False to disable. + + Returns: + A response object with the result of the command. + """ + + self.comm.send(f"enable_chuck_hover {stat}") + Response.check_resp(self.comm.read_line()) + + + def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> None: + """Enable chuck site hover height. + + The Hover height is a height that is significantly closer to the chuck compared to the separation height. + It is closer to the wafer but it is not safe for fast or long chuck moves as the chuck may be slightly + tilted. + + Args: + site (ChuckSite): The chuck site to enable hover height for. + stat (bool): True to enable, False to disable. + + Returns: + A response object with the result of the command. + """ + + self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") + Response.check_resp(self.comm.read_line()) + + + def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> None: + """Enable overtravel distance for a specific chuck site. + + Args: + site (ChuckSite): The chuck site to enable overtravel distance for. + stat (bool): True to enable, False to disable. + + Returns: + A response object with the result of the command. + """ + + self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") + Response.check_resp(self.comm.read_line()) + + + def file_transfer(self, source: str, dest: str) -> Response: + """Transfer a file to the prober. + + This function will transfer a file to the prober. The file will be stored in the position specified by + the dest argument. Transmission of the file may take some time. + + Args: + source (str): The path to the file to transfer. + dest (str): The destination path on the prober. Must be a complete path including file name. Make sure that SENTIO has write access to the given destination. + """ + # open file and encode with base64 + if not os.path.isfile(source): + raise ProberException(f"File {source} not found!") + + file_bytes = open(source, "rb").read() + encoded = base64.b64encode(file_bytes).decode("ascii") + + self.comm.send(f"file_transfer {dest}, {encoded}") + return Response.check_resp(self.comm.read_line()) + + + def get_chuck_site_height(self, site: ChuckSite) -> Tuple[float, float, float, float]: + """Retrieves height information of a chuck site + + Example: + + contact, separation, overtravel_dist, hover_gap = get_chuck_site_height(ChuckSite.Wafer) + + Gets for chuck site "Wafer" contact height, separation heights ,overtravel distance and hover height + + Args: + site (ChuckSite): The chuck site to query. + + Returns: + contact (float): contact height + separation (float): separation gap + overtravel_dist (float): overtravel distance + hover_gap (float): hover gap + """ + self.comm.send("get_chuck_site_heights {0}".format(site.toSentioAbbr())) + resp = Response.check_resp(self.comm.read_line()) + + tok = resp.message().split(",") + + contact = float(tok[0]) + separation = float(tok[1]) + overtravel_gap = float(tok[2]) + hover_gap = float(tok[3]) + + return contact, separation, overtravel_gap, hover_gap + + + def get_chuck_site_status(self, site: ChuckSite) -> Tuple[bool, bool, bool, bool]: + """Get status of a chuck site. + + Wraps SENTIO's "get_chuck_site_status" remote command. + + Args: + site (ChuckSite): The chuck site to query. + + Returns: + hasHome (bool): True if the chuck site has a home position. + hasContact (bool): True if the chuck site has a contact position. + overtravelActive (bool): True if the overtravel is active. + vacuumOn (bool): True if the vacuum is on. + """ + self.comm.send("get_chuck_site_status {0}".format(site.toSentioAbbr())) + resp = Response.check_resp(self.comm.read_line()) + + tok = resp.message().split(",") + + def str_to_bool(v: str): + if v == "0": + return False + elif v == "1": + return True + else: + raise ProberException(f"get_chuck_site_status: Invalid boolean value ({v})") + + hasHome = str_to_bool(tok[0]) + hasContact = str_to_bool(tok[1]) + overtravelActive = str_to_bool(tok[2]) + vacuumOn = str_to_bool(tok[3]) + + return hasHome, hasContact, overtravelActive, vacuumOn + + + def get_chuck_theta(self, site: ChuckSite) -> float: + """Get the current angle of the chuck. + + Wraps SENTIO's "get_chuck_theta" remote command. + + Args: + site (ChuckSite): The chuck site to query. + + Raises: + ProberException: If no available port is found. + + Returns: + angle (float): The current angle of the chuck site in degrees. + """ + + self.comm.send("get_chuck_theta {0}".format(site.toSentioAbbr())) + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + + def get_chuck_xy(self, site: ChuckSite, ref: ChuckXYReference) -> Tuple[float, float]: + """Get current chuck xy position with respect to a given reference. + + Args: + site (ChuckSite): The chuck site to query. + ref (ChuckXYReference): The reference to use for the query. + + Returns: + x (float): x position in micrometer. + y (float): y position in micrometer. + """ + + if site is None: + self.comm.send(f"get_chuck_xy {site.toSentioAbbr()}") + else: + self.comm.send(f"get_chuck_xy {site.toSentioAbbr()}, {ref.toSentioAbbr()}") + + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + + @deprecated(reason="Duplicate functionality; Use get_chuck_xy instead") + def get_chuck_xy_pos(self) -> Tuple[float, float]: + self.comm.send("get_chuck_xy") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + curX = float(tok[0]) + curY = float(tok[1]) + return curX, curY + + + def get_chuck_z(self, ref: ChuckZReference) -> float: + """Get chuck z position. + + Args: + ref (ChuckZReference): The reference to use for the query. + + Returns: + height (float): The actual z position of the chuck in micrometer (from axis zero). + """ + + self.comm.send(f"get_chuck_z {ref.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + def get_project(self, pfi: ProjectFileInfo = ProjectFileInfo.FullPath) -> str: + """Get the name of the current project. + + Args: + pfi (ProjectFileInfo): The type of information to get. + + Returns: + project_name (str): The name of the current project. + """ + + self.comm.send(f"get_project {pfi.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def get_scope_xy(self) -> Tuple[float, float]: + """Get current scope xy position. + + The returned position is an absolute position with respect to the axis zero in micrometer. + + Returns: + x (float): The current x position in micrometer. + y (float): The current y position in micrometer. + """ + + self.comm.send("get_scope_xy") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + def get_scope_z(self) -> float: + """Get scope z position in micrometer from axis zero. + + Returns: + height (float): The z position in micrometer. + """ + + self.comm.send("get_scope_z") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + + def get_scope_site(self, idx : int) -> Tuple[str, float, float, bool]: + """ Get scope site data. + + This command queries the name and position of a scope site. The index is zero based. + + Args: + idx (int): The index of the site to query. + + Returns: + num (int): The number of defined scope sites. + """ + + self.comm.send(f"get_scope_site {idx}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + + id = tok[0] + x = float(tok[1]) + y = float(tok[2]) + + # tok[3] is the reference which should always be "home" + + enabled = tok[4].upper() == "TRUE" + + return (id, x, y, enabled) + + + def get_scope_site_num(self) -> int: + """Get total number of scope sites. + + The scope stage can store a list of sites. Each site has a position and a name. + This function returns the number of defined scope sites. + + Returns: + num (int): The number of defined scope sites. + """ + + self.comm.send("get_scope_site_num") + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) + + + def has_chuck_xyz(self) -> bool: + """Returns True if the chuck has xyz axes. + + Returns: + has_xyz (bool): True if the chuck has xyz axes. + """ + + self.comm.send("has_chuck_xyz") + resp = Response.check_resp(self.comm.read_line()) + return resp.message().upper() == "YES" + + + def has_scope_xyz(self) -> bool: + """Returns True if the scope has xyz axes. + + Returns: + has_xyz (bool): True if the scope has xyz axes. + """ + + self.comm.send("has_scope_xyz") + resp = Response.check_resp(self.comm.read_line()) + return resp.message().upper() == "YES" + + + def has_scope_z(self) -> bool: + """Returns true if the microscope has a motorized z axis. + + Returns: + True if the scope has z axes. + """ + + self.comm.send("has_scope_z") + resp = Response.check_resp(self.comm.read_line()) + return resp.message().upper() == "YES" + + + def initialize_if_needed(self): + """Initialize the prober if it is not already initialized. + + This command will check if the prober is already initialized. If not + it will start the initialization process and wait for it to complete. + + You do not have to call waitcomplete after this function on your own! + """ + isInitialized, isMeasuring, isLoaderBusy = self.status.get_machine_status() + + if not isInitialized: + resp = self.start_initialization() + + if not resp.ok(): + raise ProberException(f"Cannot start initialization: {resp.message()}") + + resp = self.wait_complete(resp.cmd_id(), 180) + if not resp.ok(): + raise ProberException(f"Initialization failed: {resp.message()}") + + + def local_mode(self): + """Switch the prober back into local mode. + + The probe station will automatically enter remote mode when a remote command is received. + It will remian in remote mode even after the script is finished. This command can be used + to switch the machine back into local mode and thus enable its UI. + """ + self.comm.send("*LOCAL") + + + def move_chuck_contact(self) -> float: + """Move the chuck to contact height. + + Wraps SENTIO's "move_chuck_contact" remote command. + + Returns: + height (float): The contact height in micrometer from chuck z axis zero. + """ + + self.comm.send("move_chuck_contact") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + + def move_chuck_home(self) -> Tuple[float, float]: + """Move chuck to its home position. + + Returns: + x (float): The x position in micrometer. + y (float): The y position in micrometer. + """ + + self.comm.send("move_chuck_home ") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + + def move_chuck_load(self, pos: LoadPosition) -> None: + """Move chuck to load position. + + Wraps SENTIO's "move_chuck_load" remote command. + + Args: + pos (LoadPosition): The position to move the chuck to. This can either be a load position or the center position of the chuck. + + Returns: + A response object with the result of the command. + """ + self.comm.send(f"move_chuck_load {pos.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + + def move_chuck_separation(self) -> float: + """Move the chuck to separation height. + + Returns: + The separation height in micrometer from chuck z axis zero. + """ + self.comm.send("move_chuck_separation ") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + + def move_chuck_site(self, site: ChuckSite) -> Tuple[float, float, float, float]: + """Moves chuck to the last active position of the selected chuck site. + + Wraps SENTIO's "move_chuck_site" remote command. + + Returns: + x (float): The x position in micrometer. + y (float): The y position in micrometer. + z (float): The z height in micrometer. + theta (float): The theta angle in degrees. + """ + self.comm.send("move_chuck_site {0}".format(site.toSentioAbbr())) + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]), float(tok[2]), float(tok[3]) + + + def move_chuck_theta(self, ref: ChuckThetaReference, angle: float) -> float: + """Move chuck theta axis to a given angle. + + Wraps SENTIO's "move_chuck_theta" remote command. + + Args: + ref: The reference to use for the move. + angle: The angle to move to in degrees. + """ + self.comm.send("move_chuck_theta {0}, {1}".format(ref.toSentioAbbr(), angle)) + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + + def move_chuck_xy(self, ref: ChuckXYReference, x: float, y: float) -> Tuple[float, float]: + """Move chuck to a given xy position. + + Wraps SENTIO's "move_chuck_xy" remote command. + + Args: + ref: The reference to use for the move. + x: The x position to move to in micrometer. + y: The y position to move to in micrometer. + + Returns: + x: The chuck x position after the move in micrometer (from zero) + y: The chuck y position after the move in micrometer (from zero) + """ + self.comm.send("move_chuck_xy {0}, {1}, {2}".format(ref.toSentioAbbr(), x, y)) + resp = Response.check_resp(self.comm.read_line()) + + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + def move_chuck_z(self, ref: ChuckZReference, z: float) -> float: + """Move chuck to a given z position. + + Wraps SENTIO's "move_chuck_z" remote command. + + Args: + ref: The z-reference to use for the move. + z: The z position to move to in micrometer. + + Returns: + The actual z position in micrometer after the move. + """ + self.comm.send("move_chuck_z {0}, {1}".format(ref.toSentioAbbr(), z)) + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + def move_chuck_work_area(self, area: WorkArea) -> None: + """Move the chuck to a given work area. + + A SENTIO probe station can have different work areas. One area is for probing. This is the default + work area and present on all probe stations. When the chuck is at probing position is is roughly + centered with respect to the platen and located in a space were probes can go into contact. + Depending on the machine type a prober may be equipped with a second work area. This + area is located under an optional off-axis camera. The off-axis camera allows + inspection of the wafer when the scope camera view is blocked by certain probe card types. + + This function wraps SENTIO's "move_chuck_work_area" remote command. + + Args: + area: The work area to move to. + + Returns: + A response object with the result of the command. + """ + self.comm.send(f"move_chuck_work_area {area.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def move_scope_xy( + self, ref: ScopeXYReference, x: float, y: float + ) -> Tuple[float, float]: + """Move scope to a given xy position. + + Args: + ref: The reference to use for the move. + x: The x position to move to in micrometer. + y: The y position to move to in micrometer. + + Returns: + x: Scope x position after the move in micrometers (from zero) + y: Scope x position after the move in micrometers (from zero) + """ + + self.comm.send(f"move_scope_xy {ref.toSentioAbbr()}, {x}, {y}") + resp = Response.check_resp(self.comm.read_line()) + + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]) + + def move_scope_lift(self, state: bool) -> None: + """Move scope to its lift position. + + The scope lift position is a position where the scope is + at its axis maximum. This position will give you the maximum + possible are of unhindered operation when changing probe cards + or other maintenance tasks. + + Args: + state: True to move to the lift position, False to move away from the lift position. + """ + self.comm.send(f"move_scope_lift {state}") + Response.check_resp(self.comm.read_line()) + + def move_scope_z(self, ref: ScopeZReference, z: float) -> float: + """Move scope to a given z position. + + Args: + ref: The reference to use for the move. + z: The z position to move to in micrometer. + + Returns: + The actual z position in micrometer after the move. + """ + self.comm.send("move_scope_z {0}, {1}".format(ref.toSentioAbbr(), z)) + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + def move_vce_z(self, stage: Stage, ref: VceZReference, z: float) -> float: + """Move VCE stage to a given z position. + + Args: + ref: The reference to use for the move. + z: The z position to move to in micrometer. + + Returns: + The actual z position in micrometer after the move. + """ + if stage != Stage.Vce and stage != Stage.Vce2: + raise ProberException( + f"This command can only be applied to vce stages! (stage={0})" + ) + + self.comm.send(f"move_vce_z {stage.toSentioAbbr()}, {ref.toSentioAbbr()}, {z}") + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + def name(self) -> str: + """Returns the name of the prober. + + Returns: + This function will always return the string "SentioProber". + """ + return self.__name + + def open_project(self, project: str, restore_heights: bool = False) -> None: + """Open a SENTIO project file. + + Wraps SENTIO's "open_project" remote command. + + Args: + project: The name or path of the project to open. If a full + path to the trex project file is given SENTIO will try to open this file. + If the argument does not contain a path SENTIO will look in its default + project folder for a matching project and open it. + restore_heights: If set to true SENTIO will restore the contact + heights from the project. Be carefull when using this option because + the contact heights may have been become invalid since creating the project + due to a probe card change. + """ + self.comm.send(f"open_project {project}, {restore_heights}") + Response.check_resp(self.comm.read_line()) + + def query_command_status(self, cmd_id: int) -> Response: + """Query the status of an async command. + + This command will send a query to the prober to get the status + of an async command. The submitted command id must be a valid id + of an async command that was previously started at the prober. + + When an async command is being executed SENTIO can receive other + remnote commands. This command is intended to be used in a polling + loop to query the status of ongoing remote commands. + + Example: + +
while True:
+            time.sleep(1)
+            resp = prober.query_command_status(cmd_id)
+            if (resp.errc()!=RemoteCommandError.CommandPending):
+                break;
+ + Args: + cmd_id: The id of the async command to query. + + Returns: + A response object with the result of the command. + """ + self.comm.send("query_command_status {0}".format(cmd_id)) + resp = Response.parse_resp(self.comm.read_line()) + return resp + + def save_config(self): + """Save the SENTIO configuration file. + + Wraps SENTIO's "save_config" remote command. + """ + self.comm.send("save_config") + Response.check_resp(self.comm.read_line()) + + def save_project(self, project: str): + """Save the current SENTIO project. + + Wraps SENTIO's "save_project" remote command. + """ + self.comm.send("save_project " + project) + Response.check_resp(self.comm.read_line()) + + + def select_module(self, module: Module, tabSheet: str | None = None) -> None: + """Activate a given SENTIO module. + + In response to this function SENTIO will switch its user interface to make + the given module the active one. + + This function wraps the "select_module" remote command of SENTIO. + + Args: + module: The module to activate. + tabSheet: The name of the module tab to activate. If None is given the default tab will be activated. + """ + if tabSheet is not None: + self.comm.send(f"select_module {module.toSentioAbbr()}, {tabSheet}") + else: + self.comm.send(f"select_module {module.toSentioAbbr()}") + + Response.check_resp(self.comm.read_line()) + + + def send_cmd(self, cmd: str) -> Response: + """Sends a command to the prober and return a response object. + + This function is intended for directly sending remote commands that + are not yet included in the python wrapper. It will send the command + and parse the respone from SENTIO. + + Do NOT send low level commands that do not have a response (i.e. "*LOCAL"). + This will lock the communication pipeline as it is waiting for a + response that never arrives. + + It will then return a Response object with the extracted data from + SENTIO's response. + + Returns: + A response object with the result of the command. + """ + self.comm.send(cmd) + return Response.check_resp(self.comm.read_line()) + + def set_chuck_site_height( + self, + site: ChuckSite, + contact: float, + separation: float, + overtravel_dist: float, + hover_gap: float, + ) -> Response: + """Sets z position information of a chuck site + + Example: + + prober.set_chuck_site_height(ChuckSite.Wafer,16000,250,20,50) + + Will set the contact height of the wafer site to 16000 µm with a separation height of 250 µm an overtravel of 20 and a hover height of 50 + + Args: + site: The chuck site to query. + contact: The new contact height in micrometer. + separation: The new separation height in micrometer. + overtravel_dist: The new overtravel distance in micrometer. + hover_gap: The new hover gap in micrometer. + """ + par: str = f"{site.toSentioAbbr()},{contact},{separation},{overtravel_dist},{hover_gap}" + self.comm.send("set_chuck_site_heights {0}".format(par)) + return Response.check_resp(self.comm.read_line()) + + + def set_ink(self, idx_inker : int) -> None: + """ Trigger the inker device. + + Wraps SENTIO's "set_ink" remote command. + + Args: + idx_inker: The index of the inker to trigger. The index is one based (use either 1 or 2). + """ + + self.comm.send(f"set_ink {idx_inker}") + Response.check_resp(self.comm.read_line()) + + + def set_stepping_contact_mode(self, mode: SteppingContactMode) -> None: + """Change the stepping contact mode. + + The stepping contact mode defines what happens during stepping over a wafer. + The following modes are available: + + * BackToContact: The chuck will step into contact position + * StepToSeparation: The chuck will step into separation position. You have to move into contact with a seaparate command. + * LockContack: The Chuck is not allowed to leave contact position automatically as part of a stepping command. You have to move into separation with a separate command bevore being able to step to the next die.. + + Args: + mode: The stepping contact mode to set. + """ + + self.comm.send(f"set_stepping_contact_mode {mode.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + + def set_vacuum(self, site: ChuckSite, stat: bool) -> None: + """Switches the vacuum of a chuck site on or off. + + Args: + site: The chuck site to switch the vacuum for. + stat: True to switch the vacuum on, False to switch it off. + + Returns: + A response object with the result of the command. + """ + + self.comm.send(f"set_vacuum {site.toSentioAbbr()}, {stat}") + Response.check_resp(self.comm.read_line()) + + + def show_hint(self, msg: str, subtext: str) -> Response: + """Show an on screen message (hint) and return immediately + + Hints are on screen messages that pop up in SENTIO's lower left corner. This hint will + disappears automatically after a few seconds. + + This function wraps SENTIO's "status:show_hint" remote command. + + Args: + msg: The message to display. + subtext: The subtext to display. + """ + self.comm.send(f'status:show_hint "{msg}", "{subtext}"') + return Response.check_resp(self.comm.read_line()) + + + def show_hint_and_wait( + self, + msg: str, + subtext: str, + button_caption: str, + timeout: int = 180, + lock_ui: bool = True, + ) -> None: + """Show an on screen message with a button wait for a button to be pressed. + + Hints pop up in SENTIO's lower left corner. This function will display a hint with a button + and only return once the button has been pressed. + + Args: + msg: The message to display. + subtext: The subtext to display. Subtext is displayed in a second line with a slightly smaller font. + button_caption: The caption of the button. + timeout: An optional timeout in seconds after which the dialog will be closed automatically. (default = 180 s) + lock_ui: An optional flag that determines wether the UI shall be locked. Most of the UI is disabled in + remote mode anyway. This button affects only the on screen interactions on the right side of the main + module view. (default = True) + """ + + self.comm.send( + f'status:start_show_hint "{msg}", "{subtext}", "{button_caption}", "{lock_ui}"' + ) + resp = Response.check_resp(self.comm.read_line()) + + # wait for button press + self.comm.send(f"wait_complete {resp.cmd_id()}, {timeout}") + Response.check_resp(self.comm.read_line()) + + + def show_message( + self, msg: str, buttons: DialogButtons, caption: str, dialog_timeout: int = 180 + ) -> DialogButtons: + """Pop up a message dialog in SENTIO and wait for the result. + + Args: + msg: The message to display. + buttons: The buttons to display. + caption: The caption of the message box. + dialog_timeout: An optional dialog timeout in seconds after which the dialog will be closed automatically. + + Returns: + The button that was as an DialogButtons enum value. + """ + self.comm.send( + "status:start_show_message {0}, {1}, {2}".format( + msg, buttons.toSentioAbbr(), caption + ) + ) + resp = Response.check_resp(self.comm.read_line()) + + # wait for button press + self.comm.send("wait_complete {0}, {1}".format(resp.cmd_id(), dialog_timeout)) + resp = Response.check_resp(self.comm.read_line()) + + if resp.message().lower() == "ok": + return DialogButtons.Ok + + if resp.message().lower() == "yes": + return DialogButtons.Yes + + if resp.message().lower() == "no": + return DialogButtons.No + + if resp.message().lower() == "cancel": + return DialogButtons.Cancel + + raise ProberException("Invalid dialog button return value") + + + def start_initialization(self) -> Response: + """Start the initialization of the probe station. + + This function will start the initialization of the prober. This is an + async command that will return immediately. + + The initialization process will take some time to complete. Use + waitcomplete to wait for the initialization to complete. + """ + self.comm.send("start_initialization") + return Response.check_resp(self.comm.read_line()) + + + def wait_all(self, timeout: int = 90) -> Response: + """Wait until all async commands have finished. + + Args: + timeout: The timeout in seconds. + + Returns: + A response object with the result of the command. + """ + self.comm.send(f"wait_all {timeout}") + return Response.check_resp(self.comm.read_line()) + + + def wait_complete(self, id_or_resp: int | Response, timeout: int = 300) -> Response: + """Wait for a single async command to complete. + + Args: + cmd_id: The id of the async command to wait for. + timeout: The timeout in seconds. + + Returns: + A response object with the result of the command. + """ + if isinstance(id_or_resp, Response): + self.comm.send(f"wait_complete {id_or_resp.cmd_id()}, {timeout}") + else: + 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 + + def get_chuck_position_hint(self) -> tuple[ChuckPositionHint, ChuckSite]: + """Get a verbal representation of the current chuck position and the active site. + + Returns: + position_hint (str): Describes the chuck's general position, e.g., "Probing", "FrontLoad", "SideLoad". + site (str): The current chuck site, e.g., "Wafer", "AuxRight", "ChuckCameraLR", etc. + """ + self.comm.send("get_chuck_position_hint") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + pos_hint_str = tok[0] + site_str = tok[1] + + pos_hint = ChuckPositionHint.fromSentioAbbr(pos_hint_str) + site = ChuckSite.fromSentioAbbr(site_str) + + return pos_hint, site + + def get_chuck_site_count(self) -> int: + """Retrieve the number of chuck sites. + + Wraps SENTIO's "get_chuck_site_count" remote command. + + Returns: + count (int): The number of chuck sites. + """ + self.comm.send("get_chuck_site_count") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + count = int(tok[0]) + + return count + + def get_chuck_site_index(self, name: str | None = None) -> int: + """Retrieve the index of a chuck site by name. If no name is provided, returns the index of the current site. + + Args: + name (str, optional): The name of the chuck site. If None, the current site index is returned. + + Returns: + index (int): The index of the chuck site. + """ + if name is None: + self.comm.send("get_chuck_site_index") + else: + self.comm.send(f"get_chuck_site_index {name}") + + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + index = int(tok[0]) + + return index + + def get_chuck_site_name(self, index: int | None = None) -> ChuckSite: + """Retrieve the name of a chuck site. If no index is given, returns the current site's name. + + Args: + index (int, optional): The index of the chuck site. If None, gets the name of the current site. + + Returns: + name (str): The name of the chuck site. + """ + if index is None: + self.comm.send("get_chuck_site_name") + else: + self.comm.send(f"get_chuck_site_name {index}") + + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_str = tok[0] + site = ChuckSite.fromSentioAbbr(site_str) + + return site + + def get_chuck_site_pos(self, site: ChuckSite | None = None) -> tuple[float, float, float]: + """Retrieve position information of a chuck site. + + Args: + site (ChuckSite, optional): The chuck site to query. If None, uses the current active site. + + Returns: + tuple[float, float, float]: (home_x, home_y, angle) in µm and degrees. + """ + if site is not None: + site_arg = site.toSentioAbbr() + self.comm.send(f"get_chuck_site_pos {site_arg}") + else: + self.comm.send("get_chuck_site_pos") + + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + home_x = float(tok[0]) + home_y = float(tok[1]) + angle = float(tok[2]) + return home_x, home_y, angle + + def get_chuck_speed(self) -> ChuckSpeed: + """Retrieve the current chuck speed setting. + + Returns: + ChuckSpeed: Enum representing current chuck speed. + """ + self.comm.send("get_chuck_speed") + resp = Response.check_resp(self.comm.read_line()) + speed_str = resp.message().split(",")[0] + + speed = ChuckSpeed.fromSentioAbbr(speed_str) + + return speed + + from sentio_prober_control.Sentio.Enumerations import ChuckSite + + def get_vacuum_status(self, site: ChuckSite | None = None) -> VacuumState: + """Get the vacuum status of a chuck site. + + Args: + site (ChuckSite, optional): The chuck site to query. If None, queries the currently active site. + + Returns: + VacuumState: Enum indicating On or Off. + """ + if site is not None: + site_arg = site.toSentioAbbr() + self.comm.send(f"get_vacuum_status {site_arg}") + else: + self.comm.send("get_vacuum_status") + + resp = Response.check_resp(self.comm.read_line()) + status = resp.message().split(",")[0] + + return VacuumState.fromSentioAbbr(status) + + def move_chuck_hover(self) -> float: + """Move the chuck to hover height. + + If hover height is not enabled, the move will not be carried out. + + Returns: + z (float): The new Z position in micrometers with respect to zero. + """ + self.comm.send("move_chuck_hover") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + z_pos = float(tok[0]) + + return z_pos + + def move_chuck_index(self, ref: str, x_steps: int, y_steps: int) -> tuple[float, float]: + """Move chuck xy by a number of index steps relative to a reference point. + + If the chuck is above separation height, it will move to separation before and return after movement. + + Args: + ref (str): The reference point ("Zero", "Home", "Relative", "Center"). Case-insensitive. + x_steps (int): Number of steps in X direction. + y_steps (int): Number of steps in Y direction. + + Returns: + new_x (float): New X position (in µm, from axis zero). + new_y (float): New Y position (in µm, from axis zero). + """ + self.comm.send(f"move_chuck_index {ref}, {x_steps}, {y_steps}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + new_x = float(tok[0]) + new_y = float(tok[1]) + + return new_x, new_y + + def move_chuck_xyt(self, x_offset: float, y_offset: float, theta_offset: float) -> None: + """Move chuck xy and theta to match pattern to field of view center. + + If chuck is above separation height, it will move to separation first and stay there after movement. + + Args: + x_offset (float): Offset in X direction (in µm). + y_offset (float): Offset in Y direction (in µm). + theta_offset (float): Offset in theta (in degrees). + + Returns: + result (str): Usually returns "OK" on success. + """ + self.comm.send(f"move_chuck_xyt {x_offset}, {y_offset}, {theta_offset}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_overtravel_gap(self, gap: float) -> None: + """Set overtravel gap for all chuck sites. + + Args: + gap (float): The overtravel gap in micrometers. + + Returns: + result (str): Typically "OK" if successfully set. + """ + self.comm.send(f"set_chuck_overtravel_gap {gap}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_separation_gap(self, gap: float) -> None: + """Set separation gap for all chuck sites. + + Args: + gap (float): The separation gap in micrometers. + + Returns: + result (str): Typically "OK" if successfully set. + """ + self.comm.send(f"set_chuck_separation_gap {gap}") + Response.check_resp(self.comm.read_line()) + + from sentio_prober_control.Sentio.Enumerations import ChuckSite + from sentio_prober_control.Sentio.Response import Response + + def set_chuck_site_overtravel_gap(self, site: ChuckSite, gap: float) -> None: + """Set overtravel gap for a specific chuck site. + + Args: + site (ChuckSite): The chuck site to apply the overtravel gap to. Cannot be None. + gap (float): Overtravel gap in micrometers. + + Raises: + ValueError: If site is None. + """ + if site is None: + raise ValueError("site is required and cannot be None") + + site_arg = site.toSentioAbbr() + self.comm.send(f"set_chuck_site_overtravel_gap {site_arg}, {gap}") + Response.check_resp(self.comm.read_line()) + + def set_chuck_site_pos( + self, + x: float | None = None, + y: float | None = None, + theta: float | None = None, + site: ChuckSite | None = None + ) -> None: + """Set home position information of a chuck site. + + Args: + x (float, optional): Home X position in µm. + y (float, optional): Home Y position in µm. + theta (float, optional): Home angle in degrees. + site (ChuckSite, optional): Site enum. If None, uses current chuck site. + + Returns: + None + """ + if x is not None and y is not None and theta is not None: + site_arg = site.toSentioAbbr() if site is not None else None + + if site_arg: + self.comm.send(f"set_chuck_site_pos {site_arg},{x},{y},{theta}") + else: + self.comm.send(f"set_chuck_site_pos {x},{y},{theta}") + else: + self.comm.send("set_chuck_site_pos") + + Response.check_resp(self.comm.read_line()) + + def set_chuck_site_separation_gap(self, site: ChuckSite, gap: float) -> None: + """Set separation gap for a specific chuck site. + + Args: + site (ChuckSite): The chuck site to apply the gap to. This parameter is required. + gap (float): Separation gap in micrometers. + + Raises: + ValueError: If site is None. + """ + if site is None: + raise ValueError("site is required and cannot be None") + + site_arg = site.toSentioAbbr() + self.comm.send(f"set_chuck_site_separation_gap {site_arg}, {gap}") + Response.check_resp(self.comm.read_line()) + + def set_high_power_air(self, state: HighPowerAirState) -> None: + """Switch the air of high power prober card on or off. + + Args: + state (HighPowerAirState): Enum value indicating whether to enable or disable air. + + Raises: + ValueError: If state is None or not a valid enum. + """ + if state is None: + raise ValueError("state is required and must be a valid HighPowerAirState enum") + + self.comm.send(f"set_high_power_air {state.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_soft_contact(self, state: SoftContactState) -> None: + """Enable or disable soft contact mode. + + Args: + state (SoftContactState): Enum indicating whether to enable or disable soft contact. + + Raises: + ValueError: If state is None. + """ + if state is None: + raise ValueError("state is required and must be a valid SoftContactState enum") + + self.comm.send(f"set_soft_contact {state.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_user_coordinate_origin(self, state: UserCoordState, x: float, y: float) -> None: + """Set the new user coordinate origin (X, Y) for chuck or scope. + + Args: + state (UserCoordState): Either UserCoordState.Chuck or UserCoordState.Scope. + x (float): X offset in micrometers. + y (float): Y offset in micrometers. + + Raises: + ValueError: If any parameter is invalid. + """ + if state is None: + raise ValueError("state is required and must be a valid UserCoordState") + + cmd = f"set_user_coordinate_origin {state.toSentioAbbr()},{x},{y}" + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + def create_project(self, path_or_name: str) -> str: + """Create a new project. Overwrites if a project with the same name exists. + + Args: + path_or_name (str): Full path or project name. + If only a name is given, it will be created in the default project directory. + + Returns: + full_path (str): The full path to the created project file (*.trex). + """ + self.comm.send(f"create_project {path_or_name}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + full_path = tok[0] + + return full_path diff --git a/sentio_prober_control/UnitTest/TestProberSentio.py b/sentio_prober_control/UnitTest/TestProberSentio.py index fa75d64..e970660 100644 --- a/sentio_prober_control/UnitTest/TestProberSentio.py +++ b/sentio_prober_control/UnitTest/TestProberSentio.py @@ -4,16 +4,6 @@ from sentio_prober_control.Sentio.ProberSentio import SentioProber from sentio_prober_control.Sentio.Response import Response -from sentio_prober_control.Sentio.Enumerations import ( - ChuckPositionHint, - ChuckSite, - ChuckSpeed, - VacuumState, - HighPowerAirState, - SoftContactState, - UserCoordState -) - class TestScopeCommandGroup(unittest.TestCase): def setUp(self): """Initialize the mock communicator and ScopeCommandGroup instance.""" @@ -71,236 +61,98 @@ def test_step_scope_site_next(self): self.assertEqual(offset_x, 1000.0) self.assertEqual(offset_y, 1000.0) - def test_get_chuck_position_hint(self): - """Test get_chuck_position_hint returns correct enums.""" - self.mock_comm.read_line.return_value = "0,0,Probing,Wafer" - position, site = self.test_prober.get_chuck_position_hint() - self.mock_comm.send.assert_called_with("get_chuck_position_hint") - self.assertEqual(position, ChuckPositionHint.Center) - self.assertEqual(site, ChuckSite.Wafer) - - def test_get_chuck_site_count(self): - """Test get_chuck_site_count returns correct integer.""" - self.mock_comm.read_line.return_value = "0,0,5" - count = self.test_prober.get_chuck_site_count() - self.mock_comm.send.assert_called_with("get_chuck_site_count") - self.assertEqual(count, 5) - - def test_get_chuck_site_index_without_name(self): - """Test get_chuck_site_index without argument returns correct index.""" - self.mock_comm.read_line.return_value = "0,0,2" - index = self.test_prober.get_chuck_site_index() - self.mock_comm.send.assert_called_with("get_chuck_site_index") - self.assertEqual(index, 2) - - def test_get_chuck_site_index_with_name(self): - """Test get_chuck_site_index with a site name returns correct index.""" - self.mock_comm.read_line.return_value = "0,0,3" - index = self.test_prober.get_chuck_site_index("Wafer") - self.mock_comm.send.assert_called_with("get_chuck_site_index Wafer") - self.assertEqual(index, 3) - - def test_get_chuck_site_name_without_index(self): - """Test get_chuck_site_name with no index.""" - self.mock_comm.read_line.return_value = "0,0,Wafer" - result = self.test_prober.get_chuck_site_name() - self.mock_comm.send.assert_called_with("get_chuck_site_name") - self.assertEqual(result, ChuckSite.Wafer) - - def test_get_chuck_site_name_with_index(self): - """Test get_chuck_site_name with specific index.""" - self.mock_comm.read_line.return_value = "0,0,AuxLeft" - result = self.test_prober.get_chuck_site_name(2) - self.mock_comm.send.assert_called_with("get_chuck_site_name 2") - self.assertEqual(result, ChuckSite.AuxLeft) - - def test_get_chuck_site_pos_with_site(self): - """Test get_chuck_site_pos with ChuckSite enum.""" - self.mock_comm.read_line.return_value = "0,0,25000,40000,-45" - x, y, theta = self.test_prober.get_chuck_site_pos(ChuckSite.Wafer) - self.mock_comm.send.assert_called_with("get_chuck_site_pos Wafer") - self.assertEqual(x, 25000.0) - self.assertEqual(y, 40000.0) - self.assertEqual(theta, -45.0) - - def test_get_chuck_site_pos_without_site(self): - """Test get_chuck_site_pos without site (use current active site).""" - self.mock_comm.read_line.return_value = "0,0,1000,2000,15" - x, y, theta = self.test_prober.get_chuck_site_pos() - self.mock_comm.send.assert_called_with("get_chuck_site_pos") - self.assertEqual(x, 1000.0) - self.assertEqual(y, 2000.0) - self.assertEqual(theta, 15.0) - - def test_get_chuck_speed(self): - """Test get_chuck_speed returns correct ChuckSpeed enum.""" - self.mock_comm.read_line.return_value = "0,0,Fast" - result = self.test_prober.get_chuck_speed() - self.mock_comm.send.assert_called_with("get_chuck_speed") - self.assertEqual(result, ChuckSpeed.Fast) - - def test_get_vacuum_status_on_with_site(self): - """Test get_vacuum_status returns ON with ChuckSite specified.""" - self.mock_comm.read_line.return_value = "0,0,1" - result = self.test_prober.get_vacuum_status(ChuckSite.AuxLeft) - self.mock_comm.send.assert_called_with("get_vacuum_status AuxLeft") - self.assertEqual(result, VacuumState.On) - - def test_get_vacuum_status_off_with_site(self): - """Test get_vacuum_status returns OFF with ChuckSite specified.""" - self.mock_comm.read_line.return_value = "0,0,0" - result = self.test_prober.get_vacuum_status(ChuckSite.Wafer) - self.mock_comm.send.assert_called_with("get_vacuum_status Wafer") - self.assertEqual(result, VacuumState.Off) - - def test_get_vacuum_status_without_site(self): - """Test get_vacuum_status returns ON using current active site.""" - self.mock_comm.read_line.return_value = "0,0,1" - result = self.test_prober.get_vacuum_status() - self.mock_comm.send.assert_called_with("get_vacuum_status") - self.assertEqual(result, VacuumState.On) + def test_get_indexer_pos(self): + self.mock_comm.read_line.return_value = "0,0,3,down" + location, position = self.test_prober.get_indexer_pos() + self.mock_comm.send.assert_called_with("get_indexer_pos") + self.assertEqual(location, 3) + self.assertEqual(position, "down") - def test_move_chuck_hover(self): - """Test move_chuck_hover returns correct Z position.""" - self.mock_comm.read_line.return_value = "0,0,16000.0" - z = self.test_prober.move_chuck_hover() - self.mock_comm.send.assert_called_with("move_chuck_hover") - self.assertEqual(z, 16000.0) - - def test_move_chuck_index(self): - """Test move_chuck_index returns correct new XY position.""" - self.mock_comm.read_line.return_value = "0,0,1000.0,3000.0" - x, y = self.test_prober.move_chuck_index("Home", 5, -3) - self.mock_comm.send.assert_called_with("move_chuck_index Home, 5, -3") - self.assertEqual(x, 1000.0) - self.assertEqual(y, 3000.0) - - def test_move_chuck_xyt(self): - """Test move_chuck_xyt sends correct command and completes without error.""" - self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.move_chuck_xyt(5.0, -10.0, 0.5) - self.mock_comm.send.assert_called_with("move_chuck_xyt 5.0, -10.0, 0.5") - self.assertIsNone(result) - - def test_set_chuck_overtravel_gap(self): - """Test set_chuck_overtravel_gap sends correct command and succeeds.""" - self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_chuck_overtravel_gap(50.0) - self.mock_comm.send.assert_called_with("set_chuck_overtravel_gap 50.0") - self.assertIsNone(result) - - def test_set_chuck_separation_gap(self): - """Test set_chuck_separation_gap sends correct command and succeeds.""" + def test_indexer_cda_on(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_chuck_separation_gap(250.0) - self.mock_comm.send.assert_called_with("set_chuck_separation_gap 250.0") - self.assertIsNone(result) + resp = self.test_prober.indexer_cda(True) + self.mock_comm.send.assert_called_with("indexer_cda on") + self.assertIsInstance(resp, Response) - def test_set_chuck_site_overtravel_gap_valid_site(self): - """Test setting overtravel gap with valid ChuckSite.""" - self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_chuck_site_overtravel_gap(ChuckSite.AuxRight, 12.5) - self.mock_comm.send.assert_called_with("set_chuck_site_overtravel_gap AuxRight, 12.5") - self.assertIsNone(result) + def test_move_bottom_platen_contact(self): + self.mock_comm.read_line.return_value = "0,0,1500" + z = self.test_prober.move_bottom_platen_contact() + self.mock_comm.send.assert_called_with("move_bottom_platen_contact") + self.assertEqual(z, 1500.0) - def test_set_chuck_site_pos_with_enum(self): - """Test set_chuck_site_pos with all values and ChuckSite enum.""" - self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_chuck_site_pos(1000.0, 2000.0, 10.0, ChuckSite.Wafer) - self.mock_comm.send.assert_called_with("set_chuck_site_pos Wafer,1000.0,2000.0,10.0") - self.assertIsNone(result) + def test_move_bottom_platen_separation(self): + self.mock_comm.read_line.return_value = "0,0,500" + z = self.test_prober.move_bottom_platen_separation() + self.mock_comm.send.assert_called_with("move_bottom_platen_separation") + self.assertEqual(z, 500.0) - def test_set_chuck_site_pos_without_site(self): - """Test set_chuck_site_pos with x, y, theta only (uses current site).""" + def test_move_indexer_lift(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_chuck_site_pos(500.0, 500.0, 0.0) - self.mock_comm.send.assert_called_with("set_chuck_site_pos 500.0,500.0,0.0") - self.assertIsNone(result) + resp = self.test_prober.move_indexer_lift() + self.mock_comm.send.assert_called_with("move_indexer_lift") + self.assertIsInstance(resp, Response) - def test_set_chuck_site_pos_with_no_arguments(self): - """Test set_chuck_site_pos with no arguments (uses current site and position).""" + def test_move_indexer_down(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_chuck_site_pos() - self.mock_comm.send.assert_called_with("set_chuck_site_pos") - self.assertIsNone(result) + resp = self.test_prober.move_indexer_down() + self.mock_comm.send.assert_called_with("move_indexer_down") + self.assertIsInstance(resp, Response) - def test_set_chuck_site_separation_gap_with_site(self): - """Test setting separation gap with ChuckSite enum.""" + def test_probe_air_lift(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_chuck_site_separation_gap(ChuckSite.AuxRight, 250.0) - self.mock_comm.send.assert_called_with("set_chuck_site_separation_gap AuxRight, 250.0") - self.assertIsNone(result) + resp = self.test_prober.probe_air_lift("v1", "lift") + self.mock_comm.send.assert_called_with("probe_air_lift v1,lift") + self.assertIsInstance(resp, Response) - def test_set_high_power_air_on(self): - """Test setting high power air to ON.""" + def test_set_signal_tower(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_high_power_air(HighPowerAirState.On) - self.mock_comm.send.assert_called_with("set_high_power_air 1") - self.assertIsNone(result) + resp = self.test_prober.set_signal_tower(1, 0, 2, -1) + self.mock_comm.send.assert_called_with("set_signal_tower 1,0,2,-1") + self.assertIsInstance(resp, Response) - def test_set_high_power_air_off(self): - """Test setting high power air to OFF.""" + def test_set_signal_tower_buzzer(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_high_power_air(HighPowerAirState.Off) - self.mock_comm.send.assert_called_with("set_high_power_air 0") - self.assertIsNone(result) + resp = self.test_prober.set_signal_tower_buzzer(2) + self.mock_comm.send.assert_called_with("set_signal_tower_buzzer 2") + self.assertIsInstance(resp, Response) - def test_set_high_power_air_none_raises(self): - """Test that passing None raises ValueError.""" - with self.assertRaises(ValueError): - self.test_prober.set_high_power_air(None) + def test_start_move_indexer_pos(self): + self.mock_comm.read_line.return_value = "0,123,OK" + resp = self.test_prober.start_move_indexer_pos(4) + self.mock_comm.send.assert_called_with("start_move_indexer_pos 4") + self.assertIsInstance(resp, Response) - def test_set_soft_contact_enable(self): - """Test enabling soft contact.""" + def test_swap_bridge(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_soft_contact(SoftContactState.Enable) - self.mock_comm.send.assert_called_with("set_soft_contact 1") - self.assertIsNone(result) + resp = self.test_prober.swap_bridge("right", "up") + self.mock_comm.send.assert_called_with("swap_bridge right,up") + self.assertIsInstance(resp, Response) - def test_set_soft_contact_disable(self): - """Test disabling soft contact.""" - self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_soft_contact(SoftContactState.Disable) - self.mock_comm.send.assert_called_with("set_soft_contact 0") - self.assertIsNone(result) + def test_get_door_status_closed_locked(self): + self.mock_comm.read_line.return_value = "0,0,1,1" + closed, locked = self.test_prober.get_door_status("prober") + self.mock_comm.send.assert_called_with("get_door_status prober") + self.assertTrue(closed) + self.assertTrue(locked) - def test_set_soft_contact_none_raises(self): - """Test passing None raises ValueError.""" - with self.assertRaises(ValueError): - self.test_prober.set_soft_contact(None) + def test_get_door_status_open_unlocked(self): + self.mock_comm.read_line.return_value = "0,0,0,0" + closed, locked = self.test_prober.get_door_status("loader") + self.mock_comm.send.assert_called_with("get_door_status loader") + self.assertFalse(closed) + self.assertFalse(locked) - def test_set_user_coordinate_origin_chuck(self): - """Test setting user coordinate origin for chuck.""" + def test_set_door_lock_lock(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_user_coordinate_origin(UserCoordState.Chuck, 0.0, 0.0) - self.mock_comm.send.assert_called_with("set_user_coordinate_origin chuck,0.0,0.0") - self.assertIsNone(result) + resp = self.test_prober.set_door_lock("prober", True) + self.mock_comm.send.assert_called_with("set_door_lock prober,1") + self.assertIsInstance(resp, Response) - def test_set_user_coordinate_origin_scope(self): - """Test setting user coordinate origin for scope.""" + def test_set_door_lock_unlock(self): self.mock_comm.read_line.return_value = "0,0,OK" - result = self.test_prober.set_user_coordinate_origin(UserCoordState.Scope, 150.0, 300.0) - self.mock_comm.send.assert_called_with("set_user_coordinate_origin scope,150.0,300.0") - self.assertIsNone(result) - - def test_set_user_coordinate_origin_none_raises(self): - """Test that passing None as state raises ValueError.""" - with self.assertRaises(ValueError): - self.test_prober.set_user_coordinate_origin(None, 0.0, 0.0) - - def test_create_project_with_full_path(self): - """Test create_project using full directory path.""" - self.mock_comm.read_line.return_value = "0,0,C:\\Users\\Project1\\Project1.trex" - result = self.test_prober.create_project("C:\\Users\\Project1") - self.mock_comm.send.assert_called_with("create_project C:\\Users\\Project1") - self.assertEqual(result, "C:\\Users\\Project1\\Project1.trex") - - def test_create_project_with_name_only(self): - """Test create_project using just project name (default directory).""" - self.mock_comm.read_line.return_value = "0,0,Default project directory\\ProjectX\\ProjectX.trex" - result = self.test_prober.create_project("ProjectX") - self.mock_comm.send.assert_called_with("create_project ProjectX") - self.assertEqual(result, "Default project directory\\ProjectX\\ProjectX.trex") + resp = self.test_prober.set_door_lock("loader", False) + self.mock_comm.send.assert_called_with("set_door_lock loader,0") + self.assertIsInstance(resp, Response) if __name__ == "__main__": diff --git a/sentio_prober_control/UnitTest/TestSetupCommandGroup.py b/sentio_prober_control/UnitTest/TestSetupCommandGroup.py new file mode 100644 index 0000000..a07d0e6 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestSetupCommandGroup.py @@ -0,0 +1,42 @@ +import unittest +from unittest.mock import MagicMock, patch +from sentio_prober_control.Sentio.CommandGroups.SetupCommandGroup import SetupCommandGroup + +class TestSetupCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_parent = MagicMock() + self.mock_comm = MagicMock() + self.mock_parent.comm = self.mock_comm + self.setup = SetupCommandGroup(self.mock_parent) + + def test_get_contact_counter(self): + self.mock_comm.read_line.return_value = "0,0,123" + resp = self.setup.get_contact_counter() + self.mock_comm.send.assert_called_with("setup:contact_counter:get") + self.assertEqual(resp, 123) + + def test_reset_contact_counter(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.setup.reset_contact_counter() + self.mock_comm.send.assert_called_with("setup:contact_counter:reset") + + def test_remote_light_off_at_contact(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.setup.remote_light_off_at_contact(True) + self.mock_comm.send.assert_called_with("setup:remote:light_off_at_contact True") + self.assertEqual(resp, "OK") + + def test_remote_light_on_at_separation(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.setup.remote_light_on_at_separation(False) + self.mock_comm.send.assert_called_with("setup:remote:light_on_at_separation False") + self.assertEqual(resp, "OK") + + def test_remote_scope_follow_off(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.setup.remote_scope_follow_off(True) + self.mock_comm.send.assert_called_with("setup:remote:scope_follow_off True") + self.assertEqual(resp, "OK") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/sentio_prober_control/UnitTest/TestStatusCommandGroup.py b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py index 02ef29f..8b487e7 100644 --- a/sentio_prober_control/UnitTest/TestStatusCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py @@ -1,91 +1,129 @@ import unittest -from unittest.mock import MagicMock -from sentio_prober_control.Sentio.Enumerations import ThermoChuckState, ChuckThermoEnergyMode, ChuckThermoHoldMode, HighPurgeState +from unittest.mock import MagicMock, patch from sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup import StatusCommandGroup -from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.Enumerations import ThermoChuckState + class TestStatusCommandGroup(unittest.TestCase): def setUp(self): - self.mock_comm = MagicMock() self.mock_parent = MagicMock() + self.mock_comm = MagicMock() self.mock_parent.comm = self.mock_comm - self.cmd = StatusCommandGroup(self.mock_parent) - - def mock_response(self, message="OK"): - mock_resp = MagicMock() - mock_resp.message.return_value = message - return mock_resp - - def test_get_chuck_temp(self): - self.mock_comm.read_line.return_value = "0,0,25.0" - Response.check_resp = MagicMock(return_value=self.mock_response("25.0")) - temp = self.cmd.get_chuck_temp() - self.assertEqual(temp, 25.0) - - def test_get_chuck_temp_setpoint(self): - Response.check_resp = MagicMock(return_value=self.mock_response("30.0")) - temp = self.cmd.get_chuck_temp_setpoint() - self.assertEqual(temp, 30.0) - - def test_get_chuck_thermo_state(self): - Response.check_resp = MagicMock(return_value=self.mock_response("cooling")) - state = self.cmd.get_chuck_thermo_state() - self.assertEqual(state, ThermoChuckState.Cooling) - - def test_get_machine_status(self): - Response.check_resp = MagicMock(return_value=self.mock_response("Ready,IsMeasuring,LoaderBusy")) - result = self.cmd.get_machine_status() - self.assertEqual(result, (True, True, True)) - - def test_get_soaking_time(self): - Response.check_resp = MagicMock(return_value=self.mock_response("60.0")) - seconds = self.cmd.get_soaking_time(80.0) - self.assertEqual(seconds, 60.0) - - def test_get_chuck_thermo_energy_mode(self): - Response.check_resp = MagicMock(return_value=self.mock_response("Fast")) - result = self.cmd.get_chuck_thermo_energy_mode() - self.assertEqual(result, ChuckThermoEnergyMode.Fast) - - def test_set_chuck_thermo_energy_mode_enum(self): - Response.check_resp = MagicMock(return_value=self.mock_response()) - resp = self.cmd.set_chuck_thermo_energy_mode(ChuckThermoEnergyMode.Optimal) - self.assertTrue(resp.message(), "OK") - - def test_set_chuck_thermo_energy_mode_str(self): - Response.check_resp = MagicMock(return_value=self.mock_response()) - resp = self.cmd.set_chuck_thermo_energy_mode("HighPower") - self.assertTrue(resp.message(), "OK") - - def test_get_chuck_thermo_hold_mode(self): - Response.check_resp = MagicMock(return_value=self.mock_response("Active")) - result = self.cmd.get_chuck_thermo_hold_mode() - self.assertEqual(result, ChuckThermoHoldMode.Active) - - def test_set_chuck_thermo_hold_mode_bool(self): - Response.check_resp = MagicMock(return_value=self.mock_response()) - resp = self.cmd.set_chuck_thermo_hold_mode(True) - self.assertTrue(resp.message(), "OK") - - def test_set_chuck_thermo_hold_mode_enum(self): - Response.check_resp = MagicMock(return_value=self.mock_response()) - resp = self.cmd.set_chuck_thermo_hold_mode(ChuckThermoHoldMode.Nonactive) - self.assertTrue(resp.message(), "OK") - - def test_get_high_purge_state(self): - Response.check_resp = MagicMock(return_value=self.mock_response("On")) - result = self.cmd.get_high_purge_state() - self.assertEqual(result, HighPurgeState.On) - - def test_set_high_purge_bool(self): - Response.check_resp = MagicMock(return_value=self.mock_response()) - resp = self.cmd.set_high_purge(False) - self.assertTrue(resp.message(), "OK") - - def test_set_high_purge_enum(self): - Response.check_resp = MagicMock(return_value=self.mock_response()) - resp = self.cmd.set_high_purge(HighPurgeState.On) - self.assertTrue(resp.message(), "OK") - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + self.status = StatusCommandGroup(self.mock_parent) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_temp(self, mock_resp): + mock_resp.return_value.message.return_value = "30.5,OK" + self.assertEqual(self.status.get_chuck_temp(), 30.5) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_temp_setpoint(self, mock_resp): + mock_resp.return_value.message.return_value = "25.0,OK" + self.assertEqual(self.status.get_chuck_temp_setpoint(), 25.0) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_thermo_state(self, mock_resp): + cases = { + "soaking": ThermoChuckState.Soaking, + "cooling": ThermoChuckState.Cooling, + "heating": ThermoChuckState.Heating, + "controlling": ThermoChuckState.Controlling, + "standby": ThermoChuckState.Standby, + "error": ThermoChuckState.Error, + "uncontrolled": ThermoChuckState.Uncontrolled, + "unknown stuff": ThermoChuckState.Unknown, + } + for msg, expected in cases.items(): + mock_resp.return_value.message.return_value = msg + self.assertEqual(self.status.get_chuck_thermo_state(), expected) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_machine_status(self, mock_resp): + mock_resp.return_value.message.return_value = "0,0,Ready,IsMeasuring,LoaderBusy" + self.assertEqual(self.status.get_machine_status(), (True, True, True)) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_soaking_time(self, mock_resp): + mock_resp.return_value.message.return_value = "120.0" + self.assertEqual(self.status.get_soaking_time(80.0), 120.0) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_temp(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + resp = self.status.set_chuck_temp(75.0) + self.assertEqual(resp, "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_thermo_energy_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "Fast" + self.assertEqual(self.status.get_chuck_thermo_energy_mode(), "Fast") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_thermo_hold_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "Active" + self.assertEqual(self.status.get_chuck_thermo_hold_mode(), "Active") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_high_purge_state(self, mock_resp): + mock_resp.return_value.message.return_value = "ON" + self.assertEqual(self.status.get_high_purge_state(), "ON") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_thermo_energy_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + resp = self.status.set_chuck_thermo_energy_mode("Optimal") + self.assertEqual(resp.message(), "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_thermo_hold_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + self.status.comm.send = MagicMock() + resp = self.status.set_chuck_thermo_hold_mode(True) + self.status.comm.send.assert_called_with("status:set_chuck_thermo_hold_mode True") + self.assertEqual(resp.message(), "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_thermo_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + resp = self.status.set_chuck_thermo_mode("Turbo") + self.assertEqual(resp.message(), "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_high_purge(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + self.status.comm.send = MagicMock() + resp = self.status.set_high_purge(True) + self.status.comm.send.assert_called_with("status:set_high_purge True") + self.assertEqual(resp.message(), "OK") + + def test_get_access_level(self): + self.mock_comm.read_line.return_value = "0,0,Admin" + self.assertEqual(self.status.get_access_level(), "Admin") + + def test_get_prop(self): + self.mock_comm.read_line.return_value = "0,0,Wafer" + self.assertEqual(self.status.get_prop("Active_Stage", "Chuck"), "Wafer") + + def test_get_machine_id(self): + self.mock_comm.read_line.return_value = "0,0,MPI12345" + self.assertEqual(self.status.get_machine_id(), "MPI12345") + + def test_get_version(self): + self.mock_comm.read_line.return_value = "0,0,v1.2.3" + self.assertEqual(self.status.get_version(), "v1.2.3") + + def test_set_prop(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.status.set_prop("Active_Stage", "Chuck", "Wafer") + self.assertEqual(resp.message(), "OK") + + def test_show_message(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.assertEqual(self.status.show_message("Confirm?", "YesNo", "ConfirmBox", "Hint"), "OK") + + def test_start_show_message(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.assertEqual(self.status.start_show_message("Start?", "OK", "StartWin", "Warning"), "OK") + +if __name__ == "__main__": + unittest.main() From fb9c3043b248fd6924e5d3d0df943aab0df8bc54 Mon Sep 17 00:00:00 2001 From: John Wu <51896886+Johnwu1020@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:06:24 +0800 Subject: [PATCH 18/41] Update TestStatusCommandGroup.py --- .../UnitTest/TestStatusCommandGroup.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/sentio_prober_control/UnitTest/TestStatusCommandGroup.py b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py index 8b487e7..7ab824c 100644 --- a/sentio_prober_control/UnitTest/TestStatusCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py @@ -1,7 +1,8 @@ import unittest from unittest.mock import MagicMock, patch from sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup import StatusCommandGroup -from sentio_prober_control.Sentio.Enumerations import ThermoChuckState +from sentio_prober_control.Sentio.Enumerations import ThermoChuckState, ChuckThermoEnergyMode +from sentio_prober_control.Sentio.Enumerations import ChuckThermoHoldMode, HighPurgeState class TestStatusCommandGroup(unittest.TestCase): @@ -49,24 +50,26 @@ def test_get_soaking_time(self, mock_resp): @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") def test_set_chuck_temp(self, mock_resp): - mock_resp.return_value.message.return_value = "OK" - resp = self.status.set_chuck_temp(75.0) - self.assertEqual(resp, "OK") + mock_resp.return_value = MagicMock() + self.status.comm.send = MagicMock() + self.status.set_chuck_temp(75.0) + self.status.comm.send.assert_called_with("status:set_chuck_temp 75.00, False") + mock_resp.assert_called() @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") def test_get_chuck_thermo_energy_mode(self, mock_resp): mock_resp.return_value.message.return_value = "Fast" - self.assertEqual(self.status.get_chuck_thermo_energy_mode(), "Fast") + self.assertEqual(self.status.get_chuck_thermo_energy_mode(), ChuckThermoEnergyMode.Fast) @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") def test_get_chuck_thermo_hold_mode(self, mock_resp): mock_resp.return_value.message.return_value = "Active" - self.assertEqual(self.status.get_chuck_thermo_hold_mode(), "Active") + self.assertEqual(self.status.get_chuck_thermo_hold_mode(), ChuckThermoHoldMode.Active) @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") def test_get_high_purge_state(self, mock_resp): - mock_resp.return_value.message.return_value = "ON" - self.assertEqual(self.status.get_high_purge_state(), "ON") + mock_resp.return_value.message.return_value = "On" + self.assertEqual(self.status.get_high_purge_state(), HighPurgeState.On) @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") def test_set_chuck_thermo_energy_mode(self, mock_resp): @@ -79,7 +82,7 @@ def test_set_chuck_thermo_hold_mode(self, mock_resp): mock_resp.return_value.message.return_value = "OK" self.status.comm.send = MagicMock() resp = self.status.set_chuck_thermo_hold_mode(True) - self.status.comm.send.assert_called_with("status:set_chuck_thermo_hold_mode True") + self.status.comm.send.assert_called_with("status:set_chuck_thermo_hold_mode Active") self.assertEqual(resp.message(), "OK") @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") @@ -93,7 +96,7 @@ def test_set_high_purge(self, mock_resp): mock_resp.return_value.message.return_value = "OK" self.status.comm.send = MagicMock() resp = self.status.set_high_purge(True) - self.status.comm.send.assert_called_with("status:set_high_purge True") + self.status.comm.send.assert_called_with("status:set_high_purge ON") self.assertEqual(resp.message(), "OK") def test_get_access_level(self): From c876a904e3bd0bcf96cdbc35e249706ad5e2f1c2 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Tue, 6 May 2025 09:47:38 +0800 Subject: [PATCH 19/41] Delete sentio_prober_control/Sentio/ProberSentio.py.bak --- .../Sentio/ProberSentio.py.bak | 1508 ----------------- 1 file changed, 1508 deletions(-) delete mode 100644 sentio_prober_control/Sentio/ProberSentio.py.bak diff --git a/sentio_prober_control/Sentio/ProberSentio.py.bak b/sentio_prober_control/Sentio/ProberSentio.py.bak deleted file mode 100644 index 8904da9..0000000 --- a/sentio_prober_control/Sentio/ProberSentio.py.bak +++ /dev/null @@ -1,1508 +0,0 @@ -import base64 -import os -from typing import Tuple -from enum import Enum - -from deprecated import deprecated - -from sentio_prober_control.Sentio.Enumerations import ( - ChuckSite, - ChuckXYReference, - ChuckThetaReference, - ChuckZReference, - DialogButtons, - LoadPosition, - ProjectFileInfo, - Module, - ScopeXYReference, - ScopeZReference, - Stage, - SteppingContactMode, - VceZReference, - WorkArea, - ChuckPositionHint, - ChuckSpeed, - VacuumState, - HighPowerAirState, - SoftContactState, - UserCoordState -) - -from sentio_prober_control.Sentio.ProberBase import ProberBase, ProberException -from sentio_prober_control.Sentio.Response import Response -from sentio_prober_control.Communication.CommunicatorBase import CommunicatorBase -from sentio_prober_control.Communication.CommunicatorGpib import CommunicatorGpib, GpibCardVendor -from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp -from sentio_prober_control.Communication.CommunicatorVisa import CommunicatorVisa -from sentio_prober_control.Sentio.CommandGroups.AuxCommandGroup import AuxCommandGroup -from sentio_prober_control.Sentio.CommandGroups.CompensationCommandGroup import CompensationCommandGroup -from sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup import LoaderCommandGroup -from sentio_prober_control.Sentio.CommandGroups.ProbeCommandGroup import ProbeCommandGroup -from sentio_prober_control.Sentio.CommandGroups.QAlibriaCommandGroup import QAlibriaCommandGroup -from sentio_prober_control.Sentio.CommandGroups.ServiceCommandGroup import ServiceCommandGroup -from sentio_prober_control.Sentio.CommandGroups.SiPHCommandGroup import SiPHCommandGroup -from sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup import StatusCommandGroup -from sentio_prober_control.Sentio.CommandGroups.VisionCommandGroup import VisionCommandGroup -from sentio_prober_control.Sentio.CommandGroups.WafermapCommandGroup import WafermapCommandGroup -from sentio_prober_control.Sentio.Enumerations import ChuckSite - - -class SentioCommunicationType(Enum): - """This enum defines different types of prober communication. - - Attributes: - BackToContact (0): Chuck will move back to contact position after stepping. - StepToSeparation (1): Chuck will move to separation position after stepping. - LockContact (2): Chuck cannot step when at contact. You will have to manually move it away from its contact position before issuing the next step command. - """ - - TcpIp = 0 - Gpib = 1 - Visa = 2 - - -class SentioProber(ProberBase): - """This class represents the SENTIO probe station in python. - It provides wrapper for most of the remote commands exposed by SENTIO. - - Attributes: - aux (AuxCommandGroup): The aux command group provides access the the aux site modules functionality. - loader (LoaderCommandGroup): The loader command group provides access to the loader modules functionality. - map (WafermapCommandGroup): The wafermap command group provides access to the wafermap modules functionality. - probe (ProbeCommandGroup): The probe command group provides access to the probe modules functionality. - qalibria (QAlibriaCommandGroup): The qalibria command group provides access to the qalibria modules functionality. - service (ServiceCommandGroup): The service command group provides access to the service modules functionality. - siph (SiPHCommandGroup): The siph command group provides access to the SiPH modules functionality. - status (StatusCommandGroup): The status command group provides access to the dashboard modules functionality. (formerly called status module) - vision (VisionCommandGroup): The vision command group provides access to the vision modules functionality. - """ - - def __init__(self, comm: CommunicatorBase): - """Construct a SENTIO prober object. - - The prober must be initialized with a communication object that - specifies how the system communicates with the probe station. - - Args: - comm (CommunicatorBase): The communicator to use for communication with the prober. - """ - ProberBase.__init__(self, comm) - - self.__name = "SentioProber" - self.comm.send("*RCS 1") # switch to the native SENTIO remote command set - - self.aux: AuxCommandGroup = AuxCommandGroup(self) - self.compensation: CompensationCommandGroup = CompensationCommandGroup(self) - self.loader: LoaderCommandGroup = LoaderCommandGroup(self) - self.map: WafermapCommandGroup = WafermapCommandGroup(self) - self.probe: ProbeCommandGroup = ProbeCommandGroup(self) - self.qalibria: QAlibriaCommandGroup = QAlibriaCommandGroup(self) - self.service: ServiceCommandGroup = ServiceCommandGroup(self) - self.siph: SiPHCommandGroup = SiPHCommandGroup(self) - self.status: StatusCommandGroup = StatusCommandGroup(self) - self.vision: VisionCommandGroup = VisionCommandGroup(self) - - - def abort_command(self, cmd_id: int) -> Response: - """Stop an ongoing asynchronous remote command. - - Args: - cmd_id (int): The id of the async command to abort. - - Returns: - A response object with the result of the command. - """ - self.comm.send("abort_command {0}".format(cmd_id)) - return Response.check_resp(self.comm.read_line()) - - - def clear_contact(self, site: ChuckSite | None = None) -> None: - """Clear contact positions. - - Args: - site (ChuckSite): The chuck site to clear. If None is given all sites will be cleared. - - Returns: - A response object with the result of the command. - """ - - if site is None: - self.comm.send("clear_contact") - else: - self.comm.send(f"clear_contact {site.toSentioAbbr()}") - - Response.check_resp(self.comm.read_line()) - - - @staticmethod - def create_prober(comm_type : str | SentioCommunicationType, arg1 : str | GpibCardVendor = "127.0.0.1:35555", arg2 : str = "") -> 'SentioProber': - """ Create an instance of a SentioProber object that is bound to a certain communication method. Your choices of communication are tcpip, gpib and visa. - - Args: - comm_type (str): The type of communication to use. Valid values are "tcpip", "gpib" and "visa". - arg1 (object): For tcpip this is a single string specifying address and port - like "127.0.0.1:35555". For visa this is the address of the device like "GPIB0::20::INSTR". For gpib two parameters are needed. - The first one is the [type of driver/card](/Communication/GpibCardVendor) installed in the system, the second parameter is the address of the device like "GPIB0:20". - arg2 (str): Only used for gpib communication. This is the GPIB address of the prober i.e. "GPIB0:20". - """ - - if comm_type == "tcpip" or comm_type == SentioCommunicationType.TcpIp: - return SentioProber(CommunicatorTcpIp.create(arg1)) - elif comm_type == "gpib" or comm_type == SentioCommunicationType.Gpib: - return SentioProber(CommunicatorGpib.create(arg1, arg2)) - elif comm_type == "visa" or comm_type == SentioCommunicationType.Visa: - return SentioProber(CommunicatorVisa.create(arg1)) - else: - raise ValueError(f'Unknown prober type: "{comm_type}"') - - - def enable_chuck_overtravel(self, stat: bool) -> None: - """Enable chuck overtravel. - - This function wraps SENTIO's "enable_chuck_overtravel" remote command. - - Args: - stat (bool): True to enable, False to disable. - - Returns: - A response object with the result of the command. - """ - - self.comm.send(f"enable_chuck_overtravel {stat}") - Response.check_resp(self.comm.read_line()) - - - def enable_chuck_hover(self, stat: bool) -> None: - """Enable chuck hover height. - - The Hover height is a height that is significantly closer to the chuck compared to the separation height. - It is closer to the wafer but it is not safe for fast or long chuck moves as the chuck may be slightly - tilted. - - This function wraps SENTIO's "enable_chuck_hover" remote command. - - Args: - stat (bool): True to enable, False to disable. - - Returns: - A response object with the result of the command. - """ - - self.comm.send(f"enable_chuck_hover {stat}") - Response.check_resp(self.comm.read_line()) - - - def enable_chuck_site_hover(self, site: ChuckSite, stat: bool) -> None: - """Enable chuck site hover height. - - The Hover height is a height that is significantly closer to the chuck compared to the separation height. - It is closer to the wafer but it is not safe for fast or long chuck moves as the chuck may be slightly - tilted. - - Args: - site (ChuckSite): The chuck site to enable hover height for. - stat (bool): True to enable, False to disable. - - Returns: - A response object with the result of the command. - """ - - self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") - Response.check_resp(self.comm.read_line()) - - - def enable_chuck_site_overtravel(self, site: ChuckSite, stat: bool) -> None: - """Enable overtravel distance for a specific chuck site. - - Args: - site (ChuckSite): The chuck site to enable overtravel distance for. - stat (bool): True to enable, False to disable. - - Returns: - A response object with the result of the command. - """ - - self.comm.send(f"enable_chuck_site_hover {site.toSentioAbbr()}, {stat}") - Response.check_resp(self.comm.read_line()) - - - def file_transfer(self, source: str, dest: str) -> Response: - """Transfer a file to the prober. - - This function will transfer a file to the prober. The file will be stored in the position specified by - the dest argument. Transmission of the file may take some time. - - Args: - source (str): The path to the file to transfer. - dest (str): The destination path on the prober. Must be a complete path including file name. Make sure that SENTIO has write access to the given destination. - """ - # open file and encode with base64 - if not os.path.isfile(source): - raise ProberException(f"File {source} not found!") - - file_bytes = open(source, "rb").read() - encoded = base64.b64encode(file_bytes).decode("ascii") - - self.comm.send(f"file_transfer {dest}, {encoded}") - return Response.check_resp(self.comm.read_line()) - - - def get_chuck_site_height(self, site: ChuckSite) -> Tuple[float, float, float, float]: - """Retrieves height information of a chuck site - - Example: - - contact, separation, overtravel_dist, hover_gap = get_chuck_site_height(ChuckSite.Wafer) - - Gets for chuck site "Wafer" contact height, separation heights ,overtravel distance and hover height - - Args: - site (ChuckSite): The chuck site to query. - - Returns: - contact (float): contact height - separation (float): separation gap - overtravel_dist (float): overtravel distance - hover_gap (float): hover gap - """ - self.comm.send("get_chuck_site_heights {0}".format(site.toSentioAbbr())) - resp = Response.check_resp(self.comm.read_line()) - - tok = resp.message().split(",") - - contact = float(tok[0]) - separation = float(tok[1]) - overtravel_gap = float(tok[2]) - hover_gap = float(tok[3]) - - return contact, separation, overtravel_gap, hover_gap - - - def get_chuck_site_status(self, site: ChuckSite) -> Tuple[bool, bool, bool, bool]: - """Get status of a chuck site. - - Wraps SENTIO's "get_chuck_site_status" remote command. - - Args: - site (ChuckSite): The chuck site to query. - - Returns: - hasHome (bool): True if the chuck site has a home position. - hasContact (bool): True if the chuck site has a contact position. - overtravelActive (bool): True if the overtravel is active. - vacuumOn (bool): True if the vacuum is on. - """ - self.comm.send("get_chuck_site_status {0}".format(site.toSentioAbbr())) - resp = Response.check_resp(self.comm.read_line()) - - tok = resp.message().split(",") - - def str_to_bool(v: str): - if v == "0": - return False - elif v == "1": - return True - else: - raise ProberException(f"get_chuck_site_status: Invalid boolean value ({v})") - - hasHome = str_to_bool(tok[0]) - hasContact = str_to_bool(tok[1]) - overtravelActive = str_to_bool(tok[2]) - vacuumOn = str_to_bool(tok[3]) - - return hasHome, hasContact, overtravelActive, vacuumOn - - - def get_chuck_theta(self, site: ChuckSite) -> float: - """Get the current angle of the chuck. - - Wraps SENTIO's "get_chuck_theta" remote command. - - Args: - site (ChuckSite): The chuck site to query. - - Raises: - ProberException: If no available port is found. - - Returns: - angle (float): The current angle of the chuck site in degrees. - """ - - self.comm.send("get_chuck_theta {0}".format(site.toSentioAbbr())) - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - - def get_chuck_xy(self, site: ChuckSite, ref: ChuckXYReference) -> Tuple[float, float]: - """Get current chuck xy position with respect to a given reference. - - Args: - site (ChuckSite): The chuck site to query. - ref (ChuckXYReference): The reference to use for the query. - - Returns: - x (float): x position in micrometer. - y (float): y position in micrometer. - """ - - if site is None: - self.comm.send(f"get_chuck_xy {site.toSentioAbbr()}") - else: - self.comm.send(f"get_chuck_xy {site.toSentioAbbr()}, {ref.toSentioAbbr()}") - - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - return float(tok[0]), float(tok[1]) - - - @deprecated(reason="Duplicate functionality; Use get_chuck_xy instead") - def get_chuck_xy_pos(self) -> Tuple[float, float]: - self.comm.send("get_chuck_xy") - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - curX = float(tok[0]) - curY = float(tok[1]) - return curX, curY - - - def get_chuck_z(self, ref: ChuckZReference) -> float: - """Get chuck z position. - - Args: - ref (ChuckZReference): The reference to use for the query. - - Returns: - height (float): The actual z position of the chuck in micrometer (from axis zero). - """ - - self.comm.send(f"get_chuck_z {ref.toSentioAbbr()}") - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - def get_project(self, pfi: ProjectFileInfo = ProjectFileInfo.FullPath) -> str: - """Get the name of the current project. - - Args: - pfi (ProjectFileInfo): The type of information to get. - - Returns: - project_name (str): The name of the current project. - """ - - self.comm.send(f"get_project {pfi.toSentioAbbr()}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - - def get_scope_xy(self) -> Tuple[float, float]: - """Get current scope xy position. - - The returned position is an absolute position with respect to the axis zero in micrometer. - - Returns: - x (float): The current x position in micrometer. - y (float): The current y position in micrometer. - """ - - self.comm.send("get_scope_xy") - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - return float(tok[0]), float(tok[1]) - - def get_scope_z(self) -> float: - """Get scope z position in micrometer from axis zero. - - Returns: - height (float): The z position in micrometer. - """ - - self.comm.send("get_scope_z") - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - - def get_scope_site(self, idx : int) -> Tuple[str, float, float, bool]: - """ Get scope site data. - - This command queries the name and position of a scope site. The index is zero based. - - Args: - idx (int): The index of the site to query. - - Returns: - num (int): The number of defined scope sites. - """ - - self.comm.send(f"get_scope_site {idx}") - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - - id = tok[0] - x = float(tok[1]) - y = float(tok[2]) - - # tok[3] is the reference which should always be "home" - - enabled = tok[4].upper() == "TRUE" - - return (id, x, y, enabled) - - - def get_scope_site_num(self) -> int: - """Get total number of scope sites. - - The scope stage can store a list of sites. Each site has a position and a name. - This function returns the number of defined scope sites. - - Returns: - num (int): The number of defined scope sites. - """ - - self.comm.send("get_scope_site_num") - resp = Response.check_resp(self.comm.read_line()) - return int(resp.message()) - - - def has_chuck_xyz(self) -> bool: - """Returns True if the chuck has xyz axes. - - Returns: - has_xyz (bool): True if the chuck has xyz axes. - """ - - self.comm.send("has_chuck_xyz") - resp = Response.check_resp(self.comm.read_line()) - return resp.message().upper() == "YES" - - - def has_scope_xyz(self) -> bool: - """Returns True if the scope has xyz axes. - - Returns: - has_xyz (bool): True if the scope has xyz axes. - """ - - self.comm.send("has_scope_xyz") - resp = Response.check_resp(self.comm.read_line()) - return resp.message().upper() == "YES" - - - def has_scope_z(self) -> bool: - """Returns true if the microscope has a motorized z axis. - - Returns: - True if the scope has z axes. - """ - - self.comm.send("has_scope_z") - resp = Response.check_resp(self.comm.read_line()) - return resp.message().upper() == "YES" - - - def initialize_if_needed(self): - """Initialize the prober if it is not already initialized. - - This command will check if the prober is already initialized. If not - it will start the initialization process and wait for it to complete. - - You do not have to call waitcomplete after this function on your own! - """ - isInitialized, isMeasuring, isLoaderBusy = self.status.get_machine_status() - - if not isInitialized: - resp = self.start_initialization() - - if not resp.ok(): - raise ProberException(f"Cannot start initialization: {resp.message()}") - - resp = self.wait_complete(resp.cmd_id(), 180) - if not resp.ok(): - raise ProberException(f"Initialization failed: {resp.message()}") - - - def local_mode(self): - """Switch the prober back into local mode. - - The probe station will automatically enter remote mode when a remote command is received. - It will remian in remote mode even after the script is finished. This command can be used - to switch the machine back into local mode and thus enable its UI. - """ - self.comm.send("*LOCAL") - - - def move_chuck_contact(self) -> float: - """Move the chuck to contact height. - - Wraps SENTIO's "move_chuck_contact" remote command. - - Returns: - height (float): The contact height in micrometer from chuck z axis zero. - """ - - self.comm.send("move_chuck_contact") - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - - def move_chuck_home(self) -> Tuple[float, float]: - """Move chuck to its home position. - - Returns: - x (float): The x position in micrometer. - y (float): The y position in micrometer. - """ - - self.comm.send("move_chuck_home ") - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - return float(tok[0]), float(tok[1]) - - - def move_chuck_load(self, pos: LoadPosition) -> None: - """Move chuck to load position. - - Wraps SENTIO's "move_chuck_load" remote command. - - Args: - pos (LoadPosition): The position to move the chuck to. This can either be a load position or the center position of the chuck. - - Returns: - A response object with the result of the command. - """ - self.comm.send(f"move_chuck_load {pos.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) - - - def move_chuck_separation(self) -> float: - """Move the chuck to separation height. - - Returns: - The separation height in micrometer from chuck z axis zero. - """ - self.comm.send("move_chuck_separation ") - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - - def move_chuck_site(self, site: ChuckSite) -> Tuple[float, float, float, float]: - """Moves chuck to the last active position of the selected chuck site. - - Wraps SENTIO's "move_chuck_site" remote command. - - Returns: - x (float): The x position in micrometer. - y (float): The y position in micrometer. - z (float): The z height in micrometer. - theta (float): The theta angle in degrees. - """ - self.comm.send("move_chuck_site {0}".format(site.toSentioAbbr())) - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - return float(tok[0]), float(tok[1]), float(tok[2]), float(tok[3]) - - - def move_chuck_theta(self, ref: ChuckThetaReference, angle: float) -> float: - """Move chuck theta axis to a given angle. - - Wraps SENTIO's "move_chuck_theta" remote command. - - Args: - ref: The reference to use for the move. - angle: The angle to move to in degrees. - """ - self.comm.send("move_chuck_theta {0}, {1}".format(ref.toSentioAbbr(), angle)) - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - - def move_chuck_xy(self, ref: ChuckXYReference, x: float, y: float) -> Tuple[float, float]: - """Move chuck to a given xy position. - - Wraps SENTIO's "move_chuck_xy" remote command. - - Args: - ref: The reference to use for the move. - x: The x position to move to in micrometer. - y: The y position to move to in micrometer. - - Returns: - x: The chuck x position after the move in micrometer (from zero) - y: The chuck y position after the move in micrometer (from zero) - """ - self.comm.send("move_chuck_xy {0}, {1}, {2}".format(ref.toSentioAbbr(), x, y)) - resp = Response.check_resp(self.comm.read_line()) - - tok = resp.message().split(",") - return float(tok[0]), float(tok[1]) - - def move_chuck_z(self, ref: ChuckZReference, z: float) -> float: - """Move chuck to a given z position. - - Wraps SENTIO's "move_chuck_z" remote command. - - Args: - ref: The z-reference to use for the move. - z: The z position to move to in micrometer. - - Returns: - The actual z position in micrometer after the move. - """ - self.comm.send("move_chuck_z {0}, {1}".format(ref.toSentioAbbr(), z)) - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - def move_chuck_work_area(self, area: WorkArea) -> None: - """Move the chuck to a given work area. - - A SENTIO probe station can have different work areas. One area is for probing. This is the default - work area and present on all probe stations. When the chuck is at probing position is is roughly - centered with respect to the platen and located in a space were probes can go into contact. - Depending on the machine type a prober may be equipped with a second work area. This - area is located under an optional off-axis camera. The off-axis camera allows - inspection of the wafer when the scope camera view is blocked by certain probe card types. - - This function wraps SENTIO's "move_chuck_work_area" remote command. - - Args: - area: The work area to move to. - - Returns: - A response object with the result of the command. - """ - self.comm.send(f"move_chuck_work_area {area.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) - - def move_scope_xy( - self, ref: ScopeXYReference, x: float, y: float - ) -> Tuple[float, float]: - """Move scope to a given xy position. - - Args: - ref: The reference to use for the move. - x: The x position to move to in micrometer. - y: The y position to move to in micrometer. - - Returns: - x: Scope x position after the move in micrometers (from zero) - y: Scope x position after the move in micrometers (from zero) - """ - - self.comm.send(f"move_scope_xy {ref.toSentioAbbr()}, {x}, {y}") - resp = Response.check_resp(self.comm.read_line()) - - tok = resp.message().split(",") - return float(tok[0]), float(tok[1]) - - def move_scope_lift(self, state: bool) -> None: - """Move scope to its lift position. - - The scope lift position is a position where the scope is - at its axis maximum. This position will give you the maximum - possible are of unhindered operation when changing probe cards - or other maintenance tasks. - - Args: - state: True to move to the lift position, False to move away from the lift position. - """ - self.comm.send(f"move_scope_lift {state}") - Response.check_resp(self.comm.read_line()) - - def move_scope_z(self, ref: ScopeZReference, z: float) -> float: - """Move scope to a given z position. - - Args: - ref: The reference to use for the move. - z: The z position to move to in micrometer. - - Returns: - The actual z position in micrometer after the move. - """ - self.comm.send("move_scope_z {0}, {1}".format(ref.toSentioAbbr(), z)) - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - def move_vce_z(self, stage: Stage, ref: VceZReference, z: float) -> float: - """Move VCE stage to a given z position. - - Args: - ref: The reference to use for the move. - z: The z position to move to in micrometer. - - Returns: - The actual z position in micrometer after the move. - """ - if stage != Stage.Vce and stage != Stage.Vce2: - raise ProberException( - f"This command can only be applied to vce stages! (stage={0})" - ) - - self.comm.send(f"move_vce_z {stage.toSentioAbbr()}, {ref.toSentioAbbr()}, {z}") - resp = Response.check_resp(self.comm.read_line()) - return float(resp.message()) - - def name(self) -> str: - """Returns the name of the prober. - - Returns: - This function will always return the string "SentioProber". - """ - return self.__name - - def open_project(self, project: str, restore_heights: bool = False) -> None: - """Open a SENTIO project file. - - Wraps SENTIO's "open_project" remote command. - - Args: - project: The name or path of the project to open. If a full - path to the trex project file is given SENTIO will try to open this file. - If the argument does not contain a path SENTIO will look in its default - project folder for a matching project and open it. - restore_heights: If set to true SENTIO will restore the contact - heights from the project. Be carefull when using this option because - the contact heights may have been become invalid since creating the project - due to a probe card change. - """ - self.comm.send(f"open_project {project}, {restore_heights}") - Response.check_resp(self.comm.read_line()) - - def query_command_status(self, cmd_id: int) -> Response: - """Query the status of an async command. - - This command will send a query to the prober to get the status - of an async command. The submitted command id must be a valid id - of an async command that was previously started at the prober. - - When an async command is being executed SENTIO can receive other - remnote commands. This command is intended to be used in a polling - loop to query the status of ongoing remote commands. - - Example: - -
while True:
-            time.sleep(1)
-            resp = prober.query_command_status(cmd_id)
-            if (resp.errc()!=RemoteCommandError.CommandPending):
-                break;
- - Args: - cmd_id: The id of the async command to query. - - Returns: - A response object with the result of the command. - """ - self.comm.send("query_command_status {0}".format(cmd_id)) - resp = Response.parse_resp(self.comm.read_line()) - return resp - - def save_config(self): - """Save the SENTIO configuration file. - - Wraps SENTIO's "save_config" remote command. - """ - self.comm.send("save_config") - Response.check_resp(self.comm.read_line()) - - def save_project(self, project: str): - """Save the current SENTIO project. - - Wraps SENTIO's "save_project" remote command. - """ - self.comm.send("save_project " + project) - Response.check_resp(self.comm.read_line()) - - - def select_module(self, module: Module, tabSheet: str | None = None) -> None: - """Activate a given SENTIO module. - - In response to this function SENTIO will switch its user interface to make - the given module the active one. - - This function wraps the "select_module" remote command of SENTIO. - - Args: - module: The module to activate. - tabSheet: The name of the module tab to activate. If None is given the default tab will be activated. - """ - if tabSheet is not None: - self.comm.send(f"select_module {module.toSentioAbbr()}, {tabSheet}") - else: - self.comm.send(f"select_module {module.toSentioAbbr()}") - - Response.check_resp(self.comm.read_line()) - - - def send_cmd(self, cmd: str) -> Response: - """Sends a command to the prober and return a response object. - - This function is intended for directly sending remote commands that - are not yet included in the python wrapper. It will send the command - and parse the respone from SENTIO. - - Do NOT send low level commands that do not have a response (i.e. "*LOCAL"). - This will lock the communication pipeline as it is waiting for a - response that never arrives. - - It will then return a Response object with the extracted data from - SENTIO's response. - - Returns: - A response object with the result of the command. - """ - self.comm.send(cmd) - return Response.check_resp(self.comm.read_line()) - - def set_chuck_site_height( - self, - site: ChuckSite, - contact: float, - separation: float, - overtravel_dist: float, - hover_gap: float, - ) -> Response: - """Sets z position information of a chuck site - - Example: - - prober.set_chuck_site_height(ChuckSite.Wafer,16000,250,20,50) - - Will set the contact height of the wafer site to 16000 µm with a separation height of 250 µm an overtravel of 20 and a hover height of 50 - - Args: - site: The chuck site to query. - contact: The new contact height in micrometer. - separation: The new separation height in micrometer. - overtravel_dist: The new overtravel distance in micrometer. - hover_gap: The new hover gap in micrometer. - """ - par: str = f"{site.toSentioAbbr()},{contact},{separation},{overtravel_dist},{hover_gap}" - self.comm.send("set_chuck_site_heights {0}".format(par)) - return Response.check_resp(self.comm.read_line()) - - - def set_ink(self, idx_inker : int) -> None: - """ Trigger the inker device. - - Wraps SENTIO's "set_ink" remote command. - - Args: - idx_inker: The index of the inker to trigger. The index is one based (use either 1 or 2). - """ - - self.comm.send(f"set_ink {idx_inker}") - Response.check_resp(self.comm.read_line()) - - - def set_stepping_contact_mode(self, mode: SteppingContactMode) -> None: - """Change the stepping contact mode. - - The stepping contact mode defines what happens during stepping over a wafer. - The following modes are available: - - * BackToContact: The chuck will step into contact position - * StepToSeparation: The chuck will step into separation position. You have to move into contact with a seaparate command. - * LockContack: The Chuck is not allowed to leave contact position automatically as part of a stepping command. You have to move into separation with a separate command bevore being able to step to the next die.. - - Args: - mode: The stepping contact mode to set. - """ - - self.comm.send(f"set_stepping_contact_mode {mode.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) - - - def set_vacuum(self, site: ChuckSite, stat: bool) -> None: - """Switches the vacuum of a chuck site on or off. - - Args: - site: The chuck site to switch the vacuum for. - stat: True to switch the vacuum on, False to switch it off. - - Returns: - A response object with the result of the command. - """ - - self.comm.send(f"set_vacuum {site.toSentioAbbr()}, {stat}") - Response.check_resp(self.comm.read_line()) - - - def show_hint(self, msg: str, subtext: str) -> Response: - """Show an on screen message (hint) and return immediately - - Hints are on screen messages that pop up in SENTIO's lower left corner. This hint will - disappears automatically after a few seconds. - - This function wraps SENTIO's "status:show_hint" remote command. - - Args: - msg: The message to display. - subtext: The subtext to display. - """ - self.comm.send(f'status:show_hint "{msg}", "{subtext}"') - return Response.check_resp(self.comm.read_line()) - - - def show_hint_and_wait( - self, - msg: str, - subtext: str, - button_caption: str, - timeout: int = 180, - lock_ui: bool = True, - ) -> None: - """Show an on screen message with a button wait for a button to be pressed. - - Hints pop up in SENTIO's lower left corner. This function will display a hint with a button - and only return once the button has been pressed. - - Args: - msg: The message to display. - subtext: The subtext to display. Subtext is displayed in a second line with a slightly smaller font. - button_caption: The caption of the button. - timeout: An optional timeout in seconds after which the dialog will be closed automatically. (default = 180 s) - lock_ui: An optional flag that determines wether the UI shall be locked. Most of the UI is disabled in - remote mode anyway. This button affects only the on screen interactions on the right side of the main - module view. (default = True) - """ - - self.comm.send( - f'status:start_show_hint "{msg}", "{subtext}", "{button_caption}", "{lock_ui}"' - ) - resp = Response.check_resp(self.comm.read_line()) - - # wait for button press - self.comm.send(f"wait_complete {resp.cmd_id()}, {timeout}") - Response.check_resp(self.comm.read_line()) - - - def show_message( - self, msg: str, buttons: DialogButtons, caption: str, dialog_timeout: int = 180 - ) -> DialogButtons: - """Pop up a message dialog in SENTIO and wait for the result. - - Args: - msg: The message to display. - buttons: The buttons to display. - caption: The caption of the message box. - dialog_timeout: An optional dialog timeout in seconds after which the dialog will be closed automatically. - - Returns: - The button that was as an DialogButtons enum value. - """ - self.comm.send( - "status:start_show_message {0}, {1}, {2}".format( - msg, buttons.toSentioAbbr(), caption - ) - ) - resp = Response.check_resp(self.comm.read_line()) - - # wait for button press - self.comm.send("wait_complete {0}, {1}".format(resp.cmd_id(), dialog_timeout)) - resp = Response.check_resp(self.comm.read_line()) - - if resp.message().lower() == "ok": - return DialogButtons.Ok - - if resp.message().lower() == "yes": - return DialogButtons.Yes - - if resp.message().lower() == "no": - return DialogButtons.No - - if resp.message().lower() == "cancel": - return DialogButtons.Cancel - - raise ProberException("Invalid dialog button return value") - - - def start_initialization(self) -> Response: - """Start the initialization of the probe station. - - This function will start the initialization of the prober. This is an - async command that will return immediately. - - The initialization process will take some time to complete. Use - waitcomplete to wait for the initialization to complete. - """ - self.comm.send("start_initialization") - return Response.check_resp(self.comm.read_line()) - - - def wait_all(self, timeout: int = 90) -> Response: - """Wait until all async commands have finished. - - Args: - timeout: The timeout in seconds. - - Returns: - A response object with the result of the command. - """ - self.comm.send(f"wait_all {timeout}") - return Response.check_resp(self.comm.read_line()) - - - def wait_complete(self, id_or_resp: int | Response, timeout: int = 300) -> Response: - """Wait for a single async command to complete. - - Args: - cmd_id: The id of the async command to wait for. - timeout: The timeout in seconds. - - Returns: - A response object with the result of the command. - """ - if isinstance(id_or_resp, Response): - self.comm.send(f"wait_complete {id_or_resp.cmd_id()}, {timeout}") - else: - 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 - - def get_chuck_position_hint(self) -> tuple[ChuckPositionHint, ChuckSite]: - """Get a verbal representation of the current chuck position and the active site. - - Returns: - position_hint (str): Describes the chuck's general position, e.g., "Probing", "FrontLoad", "SideLoad". - site (str): The current chuck site, e.g., "Wafer", "AuxRight", "ChuckCameraLR", etc. - """ - self.comm.send("get_chuck_position_hint") - resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - pos_hint_str = tok[0] - site_str = tok[1] - - pos_hint = ChuckPositionHint.fromSentioAbbr(pos_hint_str) - site = ChuckSite.fromSentioAbbr(site_str) - - return pos_hint, site - - def get_chuck_site_count(self) -> int: - """Retrieve the number of chuck sites. - - Wraps SENTIO's "get_chuck_site_count" remote command. - - Returns: - count (int): The number of chuck sites. - """ - self.comm.send("get_chuck_site_count") - resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - count = int(tok[0]) - - return count - - def get_chuck_site_index(self, name: str | None = None) -> int: - """Retrieve the index of a chuck site by name. If no name is provided, returns the index of the current site. - - Args: - name (str, optional): The name of the chuck site. If None, the current site index is returned. - - Returns: - index (int): The index of the chuck site. - """ - if name is None: - self.comm.send("get_chuck_site_index") - else: - self.comm.send(f"get_chuck_site_index {name}") - - resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - index = int(tok[0]) - - return index - - def get_chuck_site_name(self, index: int | None = None) -> ChuckSite: - """Retrieve the name of a chuck site. If no index is given, returns the current site's name. - - Args: - index (int, optional): The index of the chuck site. If None, gets the name of the current site. - - Returns: - name (str): The name of the chuck site. - """ - if index is None: - self.comm.send("get_chuck_site_name") - else: - self.comm.send(f"get_chuck_site_name {index}") - - resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - site_str = tok[0] - site = ChuckSite.fromSentioAbbr(site_str) - - return site - - def get_chuck_site_pos(self, site: ChuckSite | None = None) -> tuple[float, float, float]: - """Retrieve position information of a chuck site. - - Args: - site (ChuckSite, optional): The chuck site to query. If None, uses the current active site. - - Returns: - tuple[float, float, float]: (home_x, home_y, angle) in µm and degrees. - """ - if site is not None: - site_arg = site.toSentioAbbr() - self.comm.send(f"get_chuck_site_pos {site_arg}") - else: - self.comm.send("get_chuck_site_pos") - - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - home_x = float(tok[0]) - home_y = float(tok[1]) - angle = float(tok[2]) - return home_x, home_y, angle - - def get_chuck_speed(self) -> ChuckSpeed: - """Retrieve the current chuck speed setting. - - Returns: - ChuckSpeed: Enum representing current chuck speed. - """ - self.comm.send("get_chuck_speed") - resp = Response.check_resp(self.comm.read_line()) - speed_str = resp.message().split(",")[0] - - speed = ChuckSpeed.fromSentioAbbr(speed_str) - - return speed - - from sentio_prober_control.Sentio.Enumerations import ChuckSite - - def get_vacuum_status(self, site: ChuckSite | None = None) -> VacuumState: - """Get the vacuum status of a chuck site. - - Args: - site (ChuckSite, optional): The chuck site to query. If None, queries the currently active site. - - Returns: - VacuumState: Enum indicating On or Off. - """ - if site is not None: - site_arg = site.toSentioAbbr() - self.comm.send(f"get_vacuum_status {site_arg}") - else: - self.comm.send("get_vacuum_status") - - resp = Response.check_resp(self.comm.read_line()) - status = resp.message().split(",")[0] - - return VacuumState.fromSentioAbbr(status) - - def move_chuck_hover(self) -> float: - """Move the chuck to hover height. - - If hover height is not enabled, the move will not be carried out. - - Returns: - z (float): The new Z position in micrometers with respect to zero. - """ - self.comm.send("move_chuck_hover") - resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - z_pos = float(tok[0]) - - return z_pos - - def move_chuck_index(self, ref: str, x_steps: int, y_steps: int) -> tuple[float, float]: - """Move chuck xy by a number of index steps relative to a reference point. - - If the chuck is above separation height, it will move to separation before and return after movement. - - Args: - ref (str): The reference point ("Zero", "Home", "Relative", "Center"). Case-insensitive. - x_steps (int): Number of steps in X direction. - y_steps (int): Number of steps in Y direction. - - Returns: - new_x (float): New X position (in µm, from axis zero). - new_y (float): New Y position (in µm, from axis zero). - """ - self.comm.send(f"move_chuck_index {ref}, {x_steps}, {y_steps}") - resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - new_x = float(tok[0]) - new_y = float(tok[1]) - - return new_x, new_y - - def move_chuck_xyt(self, x_offset: float, y_offset: float, theta_offset: float) -> None: - """Move chuck xy and theta to match pattern to field of view center. - - If chuck is above separation height, it will move to separation first and stay there after movement. - - Args: - x_offset (float): Offset in X direction (in µm). - y_offset (float): Offset in Y direction (in µm). - theta_offset (float): Offset in theta (in degrees). - - Returns: - result (str): Usually returns "OK" on success. - """ - self.comm.send(f"move_chuck_xyt {x_offset}, {y_offset}, {theta_offset}") - Response.check_resp(self.comm.read_line()) - - def set_chuck_overtravel_gap(self, gap: float) -> None: - """Set overtravel gap for all chuck sites. - - Args: - gap (float): The overtravel gap in micrometers. - - Returns: - result (str): Typically "OK" if successfully set. - """ - self.comm.send(f"set_chuck_overtravel_gap {gap}") - Response.check_resp(self.comm.read_line()) - - def set_chuck_separation_gap(self, gap: float) -> None: - """Set separation gap for all chuck sites. - - Args: - gap (float): The separation gap in micrometers. - - Returns: - result (str): Typically "OK" if successfully set. - """ - self.comm.send(f"set_chuck_separation_gap {gap}") - Response.check_resp(self.comm.read_line()) - - from sentio_prober_control.Sentio.Enumerations import ChuckSite - from sentio_prober_control.Sentio.Response import Response - - def set_chuck_site_overtravel_gap(self, site: ChuckSite, gap: float) -> None: - """Set overtravel gap for a specific chuck site. - - Args: - site (ChuckSite): The chuck site to apply the overtravel gap to. Cannot be None. - gap (float): Overtravel gap in micrometers. - - Raises: - ValueError: If site is None. - """ - if site is None: - raise ValueError("site is required and cannot be None") - - site_arg = site.toSentioAbbr() - self.comm.send(f"set_chuck_site_overtravel_gap {site_arg}, {gap}") - Response.check_resp(self.comm.read_line()) - - def set_chuck_site_pos( - self, - x: float | None = None, - y: float | None = None, - theta: float | None = None, - site: ChuckSite | None = None - ) -> None: - """Set home position information of a chuck site. - - Args: - x (float, optional): Home X position in µm. - y (float, optional): Home Y position in µm. - theta (float, optional): Home angle in degrees. - site (ChuckSite, optional): Site enum. If None, uses current chuck site. - - Returns: - None - """ - if x is not None and y is not None and theta is not None: - site_arg = site.toSentioAbbr() if site is not None else None - - if site_arg: - self.comm.send(f"set_chuck_site_pos {site_arg},{x},{y},{theta}") - else: - self.comm.send(f"set_chuck_site_pos {x},{y},{theta}") - else: - self.comm.send("set_chuck_site_pos") - - Response.check_resp(self.comm.read_line()) - - def set_chuck_site_separation_gap(self, site: ChuckSite, gap: float) -> None: - """Set separation gap for a specific chuck site. - - Args: - site (ChuckSite): The chuck site to apply the gap to. This parameter is required. - gap (float): Separation gap in micrometers. - - Raises: - ValueError: If site is None. - """ - if site is None: - raise ValueError("site is required and cannot be None") - - site_arg = site.toSentioAbbr() - self.comm.send(f"set_chuck_site_separation_gap {site_arg}, {gap}") - Response.check_resp(self.comm.read_line()) - - def set_high_power_air(self, state: HighPowerAirState) -> None: - """Switch the air of high power prober card on or off. - - Args: - state (HighPowerAirState): Enum value indicating whether to enable or disable air. - - Raises: - ValueError: If state is None or not a valid enum. - """ - if state is None: - raise ValueError("state is required and must be a valid HighPowerAirState enum") - - self.comm.send(f"set_high_power_air {state.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) - - def set_soft_contact(self, state: SoftContactState) -> None: - """Enable or disable soft contact mode. - - Args: - state (SoftContactState): Enum indicating whether to enable or disable soft contact. - - Raises: - ValueError: If state is None. - """ - if state is None: - raise ValueError("state is required and must be a valid SoftContactState enum") - - self.comm.send(f"set_soft_contact {state.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) - - def set_user_coordinate_origin(self, state: UserCoordState, x: float, y: float) -> None: - """Set the new user coordinate origin (X, Y) for chuck or scope. - - Args: - state (UserCoordState): Either UserCoordState.Chuck or UserCoordState.Scope. - x (float): X offset in micrometers. - y (float): Y offset in micrometers. - - Raises: - ValueError: If any parameter is invalid. - """ - if state is None: - raise ValueError("state is required and must be a valid UserCoordState") - - cmd = f"set_user_coordinate_origin {state.toSentioAbbr()},{x},{y}" - self.comm.send(cmd) - Response.check_resp(self.comm.read_line()) - - def create_project(self, path_or_name: str) -> str: - """Create a new project. Overwrites if a project with the same name exists. - - Args: - path_or_name (str): Full path or project name. - If only a name is given, it will be created in the default project directory. - - Returns: - full_path (str): The full path to the created project file (*.trex). - """ - self.comm.send(f"create_project {path_or_name}") - resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - full_path = tok[0] - - return full_path From f871a1764aa2a97051ef33a28c9f871e66e8eb07 Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Tue, 6 May 2025 14:37:28 +0800 Subject: [PATCH 20/41] LoaderCommandGroup comment restore --- .../Sentio/CommandGroups/LoaderCommandGroup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py index f66b6e1..e1cc434 100644 --- a/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py @@ -185,7 +185,7 @@ def start_prepare_station(self, station: LoaderStation, angle: float | None = No return Response.check_resp(self.comm.read_line()) - + @deprecated("duplicate functionality; Use SentioProber.move_chuck_work_area!") def switch_work_area(self, area: str): self.comm.send("move_chuck_work_area {0}".format(area)) resp = Response.check_resp(self.comm.read_line()) From 5a3848cb4d75a07aecb0b517a2d891cdd5f356c7 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 10:55:37 +0200 Subject: [PATCH 21/41] removed Response from set_door_lock --- sentio_prober_control/Sentio/ProberSentio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index f77877d..156dd56 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -1691,7 +1691,7 @@ def get_door_status(self, door: str) -> Tuple[bool, bool]: closed, locked = resp.message().split(",") return closed == "1", locked == "1" - def set_door_lock(self, door: str, lock: bool) -> Response: + def set_door_lock(self, door: str, lock: bool) -> None: """Locks or unlocks the specified door. Args: @@ -1703,4 +1703,4 @@ def set_door_lock(self, door: str, lock: bool) -> Response: """ state = "1" if lock else "0" self.comm.send(f"set_door_lock {door.lower()},{state}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) From 946f9a38a5895086054ddba0b94e2edee2853e28 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 11:07:58 +0200 Subject: [PATCH 22/41] Fixed a couple of issues in the PR Removed response objects --- .../CommandGroups/WafermapCommandGroup.py | 1 - sentio_prober_control/Sentio/ProberSentio.py | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py index 74e3240..b946e5a 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py @@ -593,7 +593,6 @@ def get_routing(self) -> Tuple[RoutingStartPoint, RoutingPriority]: self.comm.send("map:get_routing") resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") - print(RoutingStartPoint.fromSentioAbbr(tok[0]), RoutingStartPoint.fromSentioAbbr(tok[1])) return RoutingStartPoint.fromSentioAbbr(tok[0]), RoutingStartPoint.fromSentioAbbr(tok[1]) def open(self, file_path: str) -> None: diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 156dd56..dd4f9e1 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -1637,31 +1637,31 @@ def set_signal_tower(self, red: int, yellow: int, green: int, blue: int) -> Resp self.comm.send(f"set_signal_tower {red},{yellow},{green},{blue}") return Response.check_resp(self.comm.read_line()) - def set_signal_tower_buzzer(self, state: int) -> Response: + def set_signal_tower_buzzer(self, state: int) -> None: """Set signal tower buzzer state. Args: state (int): -1=no change, 0=off, 1=on, 2=pulsed Returns: - Response: Result response. + Response: None """ self.comm.send(f"set_signal_tower_buzzer {state}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def start_move_indexer_pos(self, pos: int) -> Response: + def start_move_indexer_pos(self, pos: int) -> None: """Asynchronously move indexer to position. Args: pos (int): Position index from 1 to 5 Returns: - Response: Includes cmd_id for async tracking. + None """ self.comm.send(f"start_move_indexer_pos {pos}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def swap_bridge(self, side: str, device_position: str = None) -> Response: + def swap_bridge(self, side: str, device_position: str | None = None) -> None: """Control swap bridge side and position. Args: @@ -1669,13 +1669,13 @@ def swap_bridge(self, side: str, device_position: str = None) -> Response: device_position (str, optional): up/down Returns: - Response: Result response. + None """ cmd = f"swap_bridge {side}" if device_position: cmd += f",{device_position}" self.comm.send(cmd) - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def get_door_status(self, door: str) -> Tuple[bool, bool]: """Retrieves the closed and locked state of a door. From d2358a94f8f7582fe4b2af5bc738ff2b2ea688a7 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 11:10:20 +0200 Subject: [PATCH 23/41] Update WafermapCommandGroup.py invalid return type (should have been RoutingPriority) documentation extended --- .../Sentio/CommandGroups/WafermapCommandGroup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py index b946e5a..d6cc88a 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py @@ -589,11 +589,15 @@ def get_orient_marker(self) -> Tuple[str, float, float]: return tok[0], float(tok[1]), float(tok[2]) def get_routing(self) -> Tuple[RoutingStartPoint, RoutingPriority]: - """Retrieves routing scheme for die stepping.""" + """ Retrieves routing scheme for die stepping. + + Returns: + A tuple with the start point and the routing priority. + """ self.comm.send("map:get_routing") resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") - return RoutingStartPoint.fromSentioAbbr(tok[0]), RoutingStartPoint.fromSentioAbbr(tok[1]) + return RoutingStartPoint.fromSentioAbbr(tok[0]), RoutingPriority.fromSentioAbbr(tok[1]) def open(self, file_path: str) -> None: """Open a wafer map file.""" From 95635e44e138934dc03abc1c0e277ce793de6362 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 11:36:05 +0200 Subject: [PATCH 24/41] fix for get_chuck_site_name --- sentio_prober_control/Sentio/Enumerations.py | 2 +- sentio_prober_control/Sentio/ProberSentio.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 1a97c54..4038a73 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -331,7 +331,7 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid chuck site") @staticmethod - def fromSentioAbbr(abbr: str): + def fromSentioAbbr(abbr: str) -> "ChuckSite": mapping = { "Wafer": ChuckSite.Wafer, "AuxRight": ChuckSite.AuxRight, diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index dd4f9e1..50d0d50 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -1260,7 +1260,7 @@ def get_chuck_site_name(self, index: int | None = None) -> ChuckSite: index (int, optional): The index of the chuck site. If None, gets the name of the current site. Returns: - name (str): The name of the chuck site. + site (ChuckSite): The name of the chuck site. """ if index is None: self.comm.send("get_chuck_site_name") @@ -1268,12 +1268,8 @@ def get_chuck_site_name(self, index: int | None = None) -> ChuckSite: self.comm.send(f"get_chuck_site_name {index}") resp = Response.check_resp(self.comm.read_line()) - - # Parse response message - tok = resp.message().split(",") - site_str = tok[0] - site = ChuckSite.fromSentioAbbr(site_str) - + site : ChuckSite = ChuckSite.fromSentioAbbr(resp.message()) + return site def get_chuck_site_pos(self, site: ChuckSite | None = None) -> tuple[float, float, float]: From 24a40feb6968350d73272badedb11e2ade217ce1 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 11:42:43 +0200 Subject: [PATCH 25/41] Fixed incorrect documentation --- sentio_prober_control/Sentio/ProberSentio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 50d0d50..f04c922 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -1382,7 +1382,7 @@ def move_chuck_xyt(self, x_offset: float, y_offset: float, theta_offset: float) theta_offset (float): Offset in theta (in degrees). Returns: - result (str): Usually returns "OK" on success. + None """ self.comm.send(f"move_chuck_xyt {x_offset}, {y_offset}, {theta_offset}") Response.check_resp(self.comm.read_line()) @@ -1394,7 +1394,7 @@ def set_chuck_overtravel_gap(self, gap: float) -> None: gap (float): The overtravel gap in micrometers. Returns: - result (str): Typically "OK" if successfully set. + None """ self.comm.send(f"set_chuck_overtravel_gap {gap}") Response.check_resp(self.comm.read_line()) @@ -1406,7 +1406,7 @@ def set_chuck_separation_gap(self, gap: float) -> None: gap (float): The separation gap in micrometers. Returns: - result (str): Typically "OK" if successfully set. + None """ self.comm.send(f"set_chuck_separation_gap {gap}") Response.check_resp(self.comm.read_line()) From 338ffd1b68039833602ca5e67f5a5e8d12f1624d Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 11:48:55 +0200 Subject: [PATCH 26/41] Fixed for incorrect return value documentation --- .../Sentio/CommandGroups/WafermapCommandGroup.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py index d6cc88a..f21d698 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py @@ -316,7 +316,7 @@ def get_home_die(self) -> Tuple[int, int]: """Retrieves index coordinates of the home die. Returns: - A tuple with the die width and height, the xy offset of grid and edge area size in micrometer. + Row and column index of the home die. """ self.comm.send("map:get_home_die") resp = Response.check_resp(self.comm.read_line()) @@ -328,7 +328,7 @@ def get_num_cols(self) -> int: """Retrieves the number of columns in the grid. Returns: - A tuple with the die width and height, the xy offset of grid and edge area size in micrometer. + The number of columns in the wafer map. """ self.comm.send("map:get_num_cols") resp = Response.check_resp(self.comm.read_line()) @@ -339,7 +339,7 @@ def get_num_rows(self) -> int: """Retrieves the number of rows in the grid. Returns: - A tuple with the die width and height, the xy offset of grid and edge area size in micrometer. + The number of rows in the wafer map. """ self.comm.send("map:get_num_rows") resp = Response.check_resp(self.comm.read_line()) @@ -582,7 +582,11 @@ def step_next_die(self, site: int | None = None) -> Tuple[int, int, int]: return int(tok[0]), int(tok[1]), int(tok[2]) def get_orient_marker(self) -> Tuple[str, float, float]: - """Retrieves the type, angle, and size of the wafer orientation marker.""" + """ Retrieves the type, angle, and size of the wafer orientation marker. + + Returns: + A tuple with the marker type (Flat/Notch), angle, and size in µm. + """ self.comm.send("map:get_orient_marker") resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") From 8c12fc3a36fd3e6dcc87124a719b8a42169f566d Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 12:07:12 +0200 Subject: [PATCH 27/41] documentation fixed --- sentio_prober_control/Sentio/ProberSentio.py | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index f04c922..d865e92 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -1587,25 +1587,25 @@ def move_bottom_platen_separation(self) -> float: resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) - def move_indexer_lift(self) -> Response: + def move_indexer_lift(self) -> None: """Move indexer up. Returns: - Response: Result response. + None """ self.comm.send("move_indexer_lift") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def move_indexer_down(self) -> Response: + def move_indexer_down(self) -> None: """Move indexer down. Returns: - Response: Result response. + None """ self.comm.send("move_indexer_down") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def probe_air_lift(self, valve: str, position: str) -> Response: + def probe_air_lift(self, valve: str, position: str) -> None: """Control probe air lift mechanism. Args: @@ -1613,12 +1613,12 @@ def probe_air_lift(self, valve: str, position: str) -> Response: position (str): lift or unlift Returns: - Response: Result response. + None """ self.comm.send(f"probe_air_lift {valve},{position}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def set_signal_tower(self, red: int, yellow: int, green: int, blue: int) -> Response: + def set_signal_tower(self, red: int, yellow: int, green: int, blue: int) -> None: """Set signal tower LED states. Args: @@ -1628,10 +1628,10 @@ def set_signal_tower(self, red: int, yellow: int, green: int, blue: int) -> Resp blue (int) Returns: - Response: Result response. + None """ self.comm.send(f"set_signal_tower {red},{yellow},{green},{blue}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def set_signal_tower_buzzer(self, state: int) -> None: """Set signal tower buzzer state. From 320110e095629247b4426daca9d45cc3dab4624c Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 12:35:31 +0200 Subject: [PATCH 28/41] removed return types --- .../Sentio/CommandGroups/QAlibriaCommandGroup.py | 9 ++++++--- .../Sentio/CommandGroups/StatusCommandGroup.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py index bc79e7c..9271785 100644 --- a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py @@ -187,10 +187,13 @@ def clear_dut_network(self, dut_name: str, drift_type: DriftType, update_ui: boo Wraps SENTIO's "qal:clear_dut_network" remote command. Args: - dut_name: The name of the DUT (e.g. "RefDUT"). - drift_type: The type of drift data to clear (DriftType.DriftRef or DriftType.Drift). - update_ui: Whether to update the UI (True/False). + dut_name (str): The name of the DUT (e.g. "RefDUT"). + drift_type (DriftType): The type of drift data to clear (DriftType.DriftRef or DriftType.Drift). + update_ui (bool): Whether to update the UI (True/False). + Returns: + None + Raises: ProberException: If the remote command returns an error. """ diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 7cfc09c..9a341bb 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -172,38 +172,38 @@ def set_chuck_thermo_energy_mode(self, mode: Union[str, ChuckThermoEnergyMode]) self.comm.send(f"status:set_chuck_thermo_energy_mode {mode.toSentioAbbr()}") return Response.check_resp(self.comm.read_line()) - def set_chuck_thermo_hold_mode(self, mode: Union[bool, ChuckThermoHoldMode, str]) -> Response: + def set_chuck_thermo_hold_mode(self, mode: Union[bool, ChuckThermoHoldMode, str]) -> None: """Set thermo chuck hold mode. Args: mode: A boolean indicating whether to enable (True) or disable (False) hold mode. Returns: - A Response object confirming the command execution. + None """ if isinstance(mode, bool): mode = ChuckThermoHoldMode.Active if mode else ChuckThermoHoldMode.Nonactive elif isinstance(mode, str): mode = ChuckThermoHoldMode[mode] self.comm.send(f"status:set_chuck_thermo_hold_mode {mode.toSentioAbbr()}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def set_chuck_thermo_mode(self, mode: str) -> Response: + def set_chuck_thermo_mode(self, mode: str) -> None: """Set chuck thermo operation mode. Args: mode: The operation mode to set. Possible values: Normal, Standby, Defrost, Purge, Turbo, Eco. Returns: - A Response object confirming the command execution. + None Raises: ValueError: If the provided mode is not valid. """ self.comm.send(f"status:set_chuck_thermo_mode {mode}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def set_high_purge(self, enable: Union[bool, HighPurgeState, str]) -> Response: + def set_high_purge(self, enable: Union[bool, HighPurgeState, str]) -> None: """Set thermo chuck high purge state. Args: @@ -217,7 +217,7 @@ def set_high_purge(self, enable: Union[bool, HighPurgeState, str]) -> Response: elif isinstance(enable, str): enable = HighPurgeState[enable] self.comm.send(f"status:set_high_purge {enable.toSentioAbbr()}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def get_access_level(self) -> str: """Retrieves the access level of operation. From 132a9b57c7b061f97a9a1773d5034c02029c5cec Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Tue, 6 May 2025 12:41:01 +0200 Subject: [PATCH 29/41] return values removed --- .../CommandGroups/VisionCommandGroup.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py index 3f3272a..7150d67 100644 --- a/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py @@ -201,20 +201,20 @@ def switch_all_lights(self, stat: bool) -> None: stat: A flag indicating whether to switch the lights on or off. Returns: - A Response object. + None """ - self.comm.send("vis:switch_all_lights {0}".format(stat)) + self.comm.send(f"vis:switch_all_lights {stat}") - def remove_probetip_marker(self) -> Response: + def remove_probetip_marker(self) -> None: """Remove probetip marker from the camera display. Returns: - A Response object. + None """ self.comm.send("vis:remove_probetip_marker") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def match_tips(self, ptpa_type: PtpaType) -> Tuple[float, float]: """For internal use only! @@ -323,7 +323,11 @@ def find_thermal_die_size(self) -> Tuple[float, float]: return float(tok[0]), float(tok[1]) def get_lens_zoom_level(self) -> float: - """Get current zoom level of the lens.""" + """ Get current zoom level of the lens. + + Returns: + float: The current zoom level of the lens. + """ self.comm.send("vis:get_lens_zoom_level") resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) @@ -333,6 +337,9 @@ def set_lens_zoom_level(self, level: float) -> None: Args: level (float): Zoom level, such as 1, 2, ..., 10 + + Returns: + None """ self.comm.send(f"vis:set_lens_zoom_level {level}") Response.check_resp(self.comm.read_line()) From 15c7bab7bfba8a5ed6c0d2096f2231ead9615308 Mon Sep 17 00:00:00 2001 From: Cynthia Date: Wed, 7 May 2025 13:54:57 +0800 Subject: [PATCH 30/41] Modify the Vision/Map API based on Ingo's comments: 1. vis:get_light_status: Change the camera parameter to CameraMountPoint. 2. Removed @deprecated from the WafermapCompensationCommandGroup. 3. map:path:add_bins / map:path:remove_bins: Modify the input to accept int, list, or range. 4. Modify the unit tests for vis:get_light_status, map:path:add_bins, and map:path:remove_bins. --- .../CommandGroups/VisionCommandGroup.py | 4 +- .../WafermapCompensationCommandGroup.py | 2 - .../CommandGroups/WafermapPathCommandGroup.py | 83 +++++++++++++++---- .../UnitTest/TestVisionCommandGroup.py | 2 +- .../UnitTest/TestWafermapPathCommandGroup.py | 52 ++++++++---- 5 files changed, 110 insertions(+), 33 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py index 7150d67..b2ef23a 100644 --- a/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py @@ -344,7 +344,7 @@ def set_lens_zoom_level(self, level: float) -> None: self.comm.send(f"vis:set_lens_zoom_level {level}") Response.check_resp(self.comm.read_line()) - def get_light_status(self, camera: str) -> bool: + def get_light_status(self, camera: CameraMountPoint) -> bool: """Check whether light is on or off for a specific camera. Args: @@ -353,6 +353,6 @@ def get_light_status(self, camera: str) -> bool: Returns: True if light is ON """ - self.comm.send(f"vis:get_light_status {camera}") + self.comm.send(f"vis:get_light_status {camera.toSentioAbbr()}") resp = Response.check_resp(self.comm.read_line()) return resp.message().strip().lower() == "1" diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py index 543e87f..808a595 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py @@ -5,11 +5,9 @@ from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase -@deprecated("Use VisionCompensationGroup instead") class WafermapCompensationCommandGroup(CommandGroupBase): """This command group bundles functions for setting up and using XY/Z compensation on the wafermap.""" - @deprecated("Use vis.compensation.start_execute(CompensationType.Topography, CompensationMode.Vertical) instead") def topography(self, execute: ExecuteAction) -> Response: """Execute topography compensation. diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py index 07403c8..96d2d54 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py @@ -69,37 +69,92 @@ def set_routing(self, sp: RoutingStartPoint, pri: RoutingPriority) -> None: self.comm.send(f"map:set_routing {sp.toSentioAbbr()}, {pri.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) - def add_bins(self, selection: int | str | PathSelection) -> int: - """Adds dies with specific bin(s) to the stepping path. + def add_bins(self, selection: int | list[int] | range | list[range]) -> int: + """ + Adds dies with specific bin values to the stepping path. - Wraps SENTIO's map:path:add_bins remote command. + This method simplifies usage by accepting native Python types instead of custom strings, + making it easier to dynamically construct input data. Args: - selection: e.g. "1", "1-5", "Pass", "Unbinned" + selection (int | list[int] | range | list[range]): + - A single bin number (e.g. 1) + - A list of bin numbers (e.g. [1, 3, 5]) + - A range object (e.g. range(1, 6)) → "1-5" + - A list of ranges or mix of int/range (e.g. [range(1, 4), 10]) Returns: - Path length after addition. + int: The resulting path length after adding the dies. + + Raises: + TypeError: If the input contains unsupported data types. """ - if isinstance(selection, PathSelection): - selection_str = selection.toSentioAbbr() - else: + if isinstance(selection, int): + # Single bin value selection_str = str(selection) + elif isinstance(selection, range): + # Convert range to SENTIO format: "start-end" + selection_str = f"{selection.start}-{selection.stop - 1}" + + elif isinstance(selection, list): + parts = [] + for item in selection: + if isinstance(item, range): + parts.append(f"{item.start}-{item.stop - 1}") + elif isinstance(item, int): + parts.append(str(item)) + else: + raise TypeError(f"Unsupported item type in list: {type(item)}") + selection_str = ",".join(parts) + + else: + raise TypeError(f"Unsupported selection type: {type(selection)}") + self.comm.send(f"map:path:add_bins {selection_str}") resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) - def remove_bins(self, selection: str) -> int: - """Removes dies with specific bin(s) from stepping path. + def remove_bins(self, selection: int | list[int] | range | list[range]) -> int: + """ + Removes dies with specific bin values from the stepping path. - Wraps SENTIO's map:path:remove_bins remote command. + This method allows more natural Python inputs, avoiding manual string construction. Args: - selection: e.g. "1", "1-5", "Pass", "Fail" + selection (int | list[int] | range | list[range]): + - A single bin number (e.g. 2) + - A list of bin numbers (e.g. [1, 4, 6]) + - A range (e.g. range(5, 10)) → becomes "5-9" + - A list of ranges or combination (e.g. [range(1, 4), 6]) Returns: - Remaining path length. + int: Remaining path length after removal. + + Raises: + TypeError: If the selection type is not supported. """ - self.comm.send(f"map:path:remove_bins {selection}") + if isinstance(selection, int): + selection_str = str(selection) + + elif isinstance(selection, range): + selection_str = f"{selection.start}-{selection.stop - 1}" + + elif isinstance(selection, list): + parts = [] + for item in selection: + if isinstance(item, range): + parts.append(f"{item.start}-{item.stop - 1}") + elif isinstance(item, int): + parts.append(str(item)) + else: + raise TypeError(f"Unsupported item in list: {type(item)}") + selection_str = ",".join(parts) + + else: + raise TypeError(f"Unsupported selection type: {type(selection)}") + + self.comm.send(f"map:path:remove_bins {selection_str}") resp = Response.check_resp(self.comm.read_line()) return int(resp.message()) + diff --git a/sentio_prober_control/UnitTest/TestVisionCommandGroup.py b/sentio_prober_control/UnitTest/TestVisionCommandGroup.py index 5e86862..f98152d 100644 --- a/sentio_prober_control/UnitTest/TestVisionCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestVisionCommandGroup.py @@ -81,7 +81,7 @@ def test_snap_image_to_prober(self): def test_get_light_status(self): self.mock_comm.read_line.return_value = "0,0,1" - result = self.prober.vision.get_light_status("scope") + result = self.prober.vision.get_light_status(CameraMountPoint.Scope) self.mock_comm.send.assert_called_with("vis:get_light_status scope") self.assertTrue(result) diff --git a/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py index 51e915b..e1064f0 100644 --- a/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py @@ -45,30 +45,54 @@ def test_set_routing(self): self.prober.map.path.set_routing(RoutingStartPoint.UpperLeft, RoutingPriority.RowUniDir) self.mock_comm.send.assert_called_with("map:set_routing ul, r") - def test_add_bins_str(self): - self.mock_comm.read_line.return_value = "0,0,12" - result = self.prober.map.path.add_bins("1-5") - self.mock_comm.send.assert_called_with("map:path:add_bins 1-5") - self.assertEqual(result, 12) - def test_add_bins_int(self): self.mock_comm.read_line.return_value = "0,0,5" result = self.prober.map.path.add_bins(0) self.mock_comm.send.assert_called_with("map:path:add_bins 0") self.assertEqual(result, 5) - def test_add_bins_enum(self): - self.mock_comm.read_line.return_value = "0,0,20" - result = self.prober.map.path.add_bins(PathSelection.Pass) - self.mock_comm.send.assert_called_with("map:path:add_bins pass") - self.assertEqual(result, 20) + def test_add_bins_list_int(self): + self.mock_comm.read_line.return_value = "0,0,3" + result = self.prober.map.path.add_bins([1, 3, 5]) + self.mock_comm.send.assert_called_with("map:path:add_bins 1,3,5") + self.assertEqual(result, 3) - def test_remove_enum(self): + def test_add_bins_range(self): + self.mock_comm.read_line.return_value = "0,0,4" + result = self.prober.map.path.add_bins(range(2, 5)) + self.mock_comm.send.assert_called_with("map:path:add_bins 2-4") + self.assertEqual(result, 4) + + def test_add_bins_list_mixed(self): self.mock_comm.read_line.return_value = "0,0,6" - result = self.prober.map.path.remove_bins("Fail") - self.mock_comm.send.assert_called_with("map:path:remove_bins Fail") + result = self.prober.map.path.add_bins([range(1, 3), 4, 6]) + self.mock_comm.send.assert_called_with("map:path:add_bins 1-2,4,6") self.assertEqual(result, 6) + def test_remove_bins_int(self): + self.mock_comm.read_line.return_value = "0,0,6" + result = self.prober.map.path.remove_bins(2) + self.mock_comm.send.assert_called_with("map:path:remove_bins 2") + self.assertEqual(result, 6) + + def test_remove_bins_list(self): + self.mock_comm.read_line.return_value = "0,0,5" + result = self.prober.map.path.remove_bins([1, 4, 5]) + self.mock_comm.send.assert_called_with("map:path:remove_bins 1,4,5") + self.assertEqual(result, 5) + + def test_remove_bins_range(self): + self.mock_comm.read_line.return_value = "0,0,4" + result = self.prober.map.path.remove_bins(range(3, 6)) # 3-5 + self.mock_comm.send.assert_called_with("map:path:remove_bins 3-5") + self.assertEqual(result, 4) + + def test_remove_bins_list_mixed(self): + self.mock_comm.read_line.return_value = "0,0,7" + result = self.prober.map.path.remove_bins([range(1, 3), 6, 9]) + self.mock_comm.send.assert_called_with("map:path:remove_bins 1-2,6,9") + self.assertEqual(result, 7) + if __name__ == "__main__": unittest.main() From a3966bcf6ac30dc8c41da8c053b0d284a87e1263 Mon Sep 17 00:00:00 2001 From: Cynthia Date: Wed, 7 May 2025 15:21:15 +0800 Subject: [PATCH 31/41] Change the return value of map:get_orient_marker to enum 1. The return value of map:get_orient_marker is changed to OrientationMarker. 2. Modify the unit tests for map:get_orient_marker. 3. Removed the print statement from test_get_bin_info(). --- .../Sentio/CommandGroups/WafermapCommandGroup.py | 6 +++--- .../UnitTest/TestWafermapBinsCommandGroup.py | 1 - sentio_prober_control/UnitTest/TestWafermapCommandGroup.py | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py index f21d698..adc12a6 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py @@ -1,7 +1,7 @@ from typing import Tuple from sentio_prober_control.Sentio.Enumerations import AxisOrient, ColorScheme, DieNumber, StatusBits, RoutingStartPoint, \ - RoutingPriority + RoutingPriority, OrientationMarker from sentio_prober_control.Sentio.ProberBase import ProberException from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ModuleCommandGroupBase @@ -581,7 +581,7 @@ def step_next_die(self, site: int | None = None) -> Tuple[int, int, int]: tok = resp.message().split(",") return int(tok[0]), int(tok[1]), int(tok[2]) - def get_orient_marker(self) -> Tuple[str, float, float]: + def get_orient_marker(self) -> Tuple[OrientationMarker, float, float]: """ Retrieves the type, angle, and size of the wafer orientation marker. Returns: @@ -590,7 +590,7 @@ def get_orient_marker(self) -> Tuple[str, float, float]: self.comm.send("map:get_orient_marker") resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") - return tok[0], float(tok[1]), float(tok[2]) + return OrientationMarker[tok[0]], float(tok[1]), float(tok[2]) def get_routing(self) -> Tuple[RoutingStartPoint, RoutingPriority]: """ Retrieves routing scheme for die stepping. diff --git a/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py index 809ec32..d6c06fa 100644 --- a/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py @@ -36,7 +36,6 @@ def test_get_bin_info(self): """Test getting bin information""" self.mock_comm.read_line.return_value = "0,0,3,TestBin,Pass,#FF00FFFF" result = self.test_prober.map.bins.get_bin_info(3) - print(result) self.mock_comm.send.assert_called_with("map:bins:get_bin_info 3") self.assertEqual(result, (3, "TestBin", BinQuality.Pass, "#FF00FFFF")) diff --git a/sentio_prober_control/UnitTest/TestWafermapCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapCommandGroup.py index 9820378..58ac073 100644 --- a/sentio_prober_control/UnitTest/TestWafermapCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestWafermapCommandGroup.py @@ -3,7 +3,7 @@ from sentio_prober_control.Sentio.ProberSentio import SentioProber from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp from sentio_prober_control.Sentio.Enumerations import AxisOrient, ColorScheme, DieNumber, RoutingStartPoint, \ - RoutingPriority + RoutingPriority, OrientationMarker class TestWafermapCommandGroup(unittest.TestCase): @@ -199,7 +199,7 @@ def test_get_orient_marker(self): self.mock_comm.read_line.return_value = "0,0,Flat,90.0,1200.0" marker = self.prober.map.get_orient_marker() self.mock_comm.send.assert_called_with("map:get_orient_marker") - self.assertEqual(marker, ("Flat", 90.0, 1200.0)) + self.assertEqual(marker, (OrientationMarker.Flat, 90.0, 1200.0)) def test_set_orient_marker(self): self.mock_comm.read_line.return_value = "0,0,OK" From f845c671747d114332e5b75589d885dbfc62156b Mon Sep 17 00:00:00 2001 From: MPIDavidLiu Date: Thu, 8 May 2025 16:41:10 +0800 Subject: [PATCH 32/41] restore the statuscommandgroup and enumerations. --- .../CommandGroups/StatusCommandGroup.py | 65 +++++++------------ sentio_prober_control/Sentio/Enumerations.py | 11 ---- 2 files changed, 25 insertions(+), 51 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 9a341bb..326baa7 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -1,10 +1,5 @@ from typing import Tuple, Union -from sentio_prober_control.Sentio.Enumerations import ( - ThermoChuckState, - ChuckThermoEnergyMode, - ChuckThermoHoldMode, - HighPurgeState -) +from sentio_prober_control.Sentio.Enumerations import ThermoChuckState from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ( ModuleCommandGroupBase, @@ -126,16 +121,16 @@ def set_chuck_temp(self, temp: float, lift_chuck : bool = False) -> None: self.comm.send(f"status:set_chuck_temp {temp:.2f}, {lift_chuck}") Response.check_resp(self.comm.read_line()) - def get_chuck_thermo_energy_mode(self) -> ChuckThermoEnergyMode: + def get_chuck_thermo_energy_mode(self) -> str: """Get the current chuck thermo energy mode. Returns: The current energy mode as a string. Possible values: Fast, Optimal, HighPower, Customized. """ self.comm.send("status:get_chuck_thermo_energy_mode") resp = Response.check_resp(self.comm.read_line()) - return ChuckThermoEnergyMode[resp.message()] + return resp.message() - def get_chuck_thermo_hold_mode(self) -> ChuckThermoHoldMode: + def get_chuck_thermo_hold_mode(self) -> str: """Get thermo chuck hold mode. Returns: @@ -143,9 +138,9 @@ def get_chuck_thermo_hold_mode(self) -> ChuckThermoHoldMode: """ self.comm.send("status:get_chuck_thermo_hold_mode") resp = Response.check_resp(self.comm.read_line()) - return ChuckThermoHoldMode[resp.message()] + return resp.message() - def get_high_purge_state(self) -> HighPurgeState: + def get_high_purge_state(self) -> str: """Get thermo chuck high purge state. Returns: @@ -153,9 +148,9 @@ def get_high_purge_state(self) -> HighPurgeState: """ self.comm.send("status:get_high_purge_state") resp = Response.check_resp(self.comm.read_line()) - return HighPurgeState[resp.message()] + return resp.message() - def set_chuck_thermo_energy_mode(self, mode: Union[str, ChuckThermoEnergyMode]) -> Response: + def set_chuck_thermo_energy_mode(self, mode: str) -> Response: """Set chuck thermo energy mode. Args: @@ -167,57 +162,47 @@ def set_chuck_thermo_energy_mode(self, mode: Union[str, ChuckThermoEnergyMode]) Raises: ValueError: If the provided mode is not valid. """ - if not isinstance(mode, ChuckThermoEnergyMode): - mode = ChuckThermoEnergyMode[mode] - self.comm.send(f"status:set_chuck_thermo_energy_mode {mode.toSentioAbbr()}") + self.comm.send(f"status:set_chuck_thermo_energy_mode {mode}") return Response.check_resp(self.comm.read_line()) - def set_chuck_thermo_hold_mode(self, mode: Union[bool, ChuckThermoHoldMode, str]) -> None: + def set_chuck_thermo_hold_mode(self, mode: bool) -> Response: """Set thermo chuck hold mode. Args: mode: A boolean indicating whether to enable (True) or disable (False) hold mode. Returns: - None + A Response object confirming the command execution. """ - if isinstance(mode, bool): - mode = ChuckThermoHoldMode.Active if mode else ChuckThermoHoldMode.Nonactive - elif isinstance(mode, str): - mode = ChuckThermoHoldMode[mode] - self.comm.send(f"status:set_chuck_thermo_hold_mode {mode.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) + self.comm.send(f"status:set_chuck_thermo_hold_mode {mode}") + return Response.check_resp(self.comm.read_line()) - def set_chuck_thermo_mode(self, mode: str) -> None: + def set_chuck_thermo_mode(self, mode: str) -> Response: """Set chuck thermo operation mode. Args: mode: The operation mode to set. Possible values: Normal, Standby, Defrost, Purge, Turbo, Eco. Returns: - None + A Response object confirming the command execution. Raises: ValueError: If the provided mode is not valid. """ self.comm.send(f"status:set_chuck_thermo_mode {mode}") - Response.check_resp(self.comm.read_line()) + return Response.check_resp(self.comm.read_line()) - def set_high_purge(self, enable: Union[bool, HighPurgeState, str]) -> None: + def set_high_purge(self, enable: bool) -> Response: """Set thermo chuck high purge state. - Args: - enable: A boolean indicating whether to enable (True) or disable (False) high purge. - - Returns: - A Response object confirming the command execution. - """ - if isinstance(enable, bool): - enable = HighPurgeState.On if enable else HighPurgeState.Off - elif isinstance(enable, str): - enable = HighPurgeState[enable] - self.comm.send(f"status:set_high_purge {enable.toSentioAbbr()}") - Response.check_resp(self.comm.read_line()) + Args: + enable: A boolean indicating whether to enable (True) or disable (False) high purge + Returns: + A Response object confirming the command execution. + """ + + self.comm.send(f"status:set_high_purge {enable}") + return Response.check_resp(self.comm.read_line()) def get_access_level(self) -> str: """Retrieves the access level of operation. diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 4038a73..8267f96 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -1891,17 +1891,6 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid ChuckThermoHoldMode") -class HighPurgeState(Enum): - On = 0 - Off = 1 - - def toSentioAbbr(self): - switcher = { - HighPurgeState.On: "ON", - HighPurgeState.Off: "OFF", - } - return switcher.get(self, "Invalid HighPurgeState") - class ChuckSpeed(Enum): Fast = 0 Normal = 1 From ac1bd6036e7b31321af6613aa5b2a4d3f8fb1c74 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 11:47:37 +0200 Subject: [PATCH 33/41] Made Python api consistent with remote command api - created two new command groups for setup in order to match hierarchy of the remote command - remote return values from "remote" command group functions --- .../Sentio/CommandGroups/SetupCommandGroup.py | 69 ++----------------- .../SetupContactCounterCommandGroup.py | 29 ++++++++ .../CommandGroups/SetupRemoteCommandGroup.py | 44 ++++++++++++ 3 files changed, 79 insertions(+), 63 deletions(-) create mode 100644 sentio_prober_control/Sentio/CommandGroups/SetupContactCounterCommandGroup.py create mode 100644 sentio_prober_control/Sentio/CommandGroups/SetupRemoteCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py index 6c6c90b..a9c71a3 100644 --- a/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py @@ -1,69 +1,12 @@ -from typing import Tuple - -from sentio_prober_control.Sentio.Enumerations import ThermoChuckState -from sentio_prober_control.Sentio.Response import Response -from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ( - ModuleCommandGroupBase, -) +from sentio_prober_control.Sentio.CommandGroups.SetupContactCounterCommandGroup import SetupContactCounterCommandGroup +from sentio_prober_control.Sentio.CommandGroups.SetupRemoteCommandGroup import SetupRemoteCommandGroup +from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ModuleCommandGroupBase class SetupCommandGroup(ModuleCommandGroupBase): """A command group for accessing setup module functions.""" def __init__(self, comm) -> None: super().__init__(comm, "setup") - - def get_contact_counter(self) -> int: - """Retrieves the contact counter value. - - Returns: - An integer representing the number of times the chuck moved into contact height, - excluding moves on cleaning substrate. - """ - self.comm.send("setup:contact_counter:get") - resp = Response.check_resp(self.comm.read_line()) - return int(resp.message()) - - def reset_contact_counter(self) -> None: - """Resets the contact counter.""" - self.comm.send("setup:contact_counter:reset") - Response.check_resp(self.comm.read_line()) - - def remote_light_off_at_contact(self, status: bool) -> str: - """Defines whether light is switched off at contact height in remote mode. - - Args: - status (BooleanStatus): True/False, ON/OFF as defined in Enum. - - Returns: - Response: Response object for result checking. - """ - self.comm.send(f"setup:remote:light_off_at_contact {status}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - - def remote_light_on_at_separation(self, status: bool) -> str: - """Defines whether light is switched on at separation height in remote mode. - - Args: - status (BooleanStatus): True/False, ON/OFF as defined in Enum. - - Returns: - Response: Response object for result checking. - """ - self.comm.send(f"setup:remote:light_on_at_separation {status}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - - def remote_scope_follow_off(self, status: bool) -> str: - """Defines whether scope follow mode is switched off in remote mode. - - Args: - status (BooleanStatus): True/False, ON/OFF as defined in Enum. - Note: ON disables scope follow. - - Returns: - Response: Response object for result checking. - """ - self.comm.send(f"setup:remote:scope_follow_off {status}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + + self.contact_counter: SetupContactCounterCommandGroup = SetupContactCounterCommandGroup(comm) + self.remote: SetupRemoteCommandGroup = SetupRemoteCommandGroup(comm) \ No newline at end of file diff --git a/sentio_prober_control/Sentio/CommandGroups/SetupContactCounterCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SetupContactCounterCommandGroup.py new file mode 100644 index 0000000..7c0c4ea --- /dev/null +++ b/sentio_prober_control/Sentio/CommandGroups/SetupContactCounterCommandGroup.py @@ -0,0 +1,29 @@ +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase + + +class SetupContactCounterCommandGroup(CommandGroupBase): + """This command group bundles functions setting up the contact counter.""" + + def __init__(self, comm) -> None: + super().__init__(comm) + + def get(self) -> int: + """ Retrieves the contact counter value. + + Returns: + An integer representing the number of times the chuck moved into contact height, + excluding moves on cleaning substrate. + """ + self.comm.send("setup:contact_counter:get") + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) + + def reset(self) -> None: + """ Resets the contact counter. + + Returns: + None + """ + self.comm.send("setup:contact_counter:reset") + Response.check_resp(self.comm.read_line()) diff --git a/sentio_prober_control/Sentio/CommandGroups/SetupRemoteCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SetupRemoteCommandGroup.py new file mode 100644 index 0000000..791860a --- /dev/null +++ b/sentio_prober_control/Sentio/CommandGroups/SetupRemoteCommandGroup.py @@ -0,0 +1,44 @@ +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase + +class SetupRemoteCommandGroup(CommandGroupBase): + """ This command group bundles functions setting up the behavior of SENTIO in remote mode. """ + def __init__(self, comm) -> None: + super().__init__(comm) + + def light_off_at_contact(self, status: bool) -> None: + """Defines whether light is switched off at contact height in remote mode. + + Args: + status (BooleanStatus): True/False, ON/OFF as defined in Enum. + + Returns: + None + """ + self.comm.send(f"setup:remote:light_off_at_contact {status}") + Response.check_resp(self.comm.read_line()) + + def light_on_at_separation(self, status: bool) -> None: + """Defines whether light is switched on at separation height in remote mode. + + Args: + status (BooleanStatus): True/False, ON/OFF as defined in Enum. + + Returns: + None + """ + self.comm.send(f"setup:remote:light_on_at_separation {status}") + Response.check_resp(self.comm.read_line()) + + def scope_follow_off(self, status: bool) -> None: + """Defines whether scope follow mode is switched off in remote mode. + + Args: + status (BooleanStatus): True/False, ON/OFF as defined in Enum. + Note: ON disables scope follow. + + Returns: + Response: Response object for result checking. + """ + self.comm.send(f"setup:remote:scope_follow_off {status}") + Response.check_resp(self.comm.read_line()) From 3a6aabc8c27dc1b697ec027bcd2de4525dd53ac7 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 12:19:36 +0200 Subject: [PATCH 34/41] Work on Review issues Enumerations should be sorted alphabetically. Otherwise the python documentation is out of order. start_show_message should return a response object --- .../CommandGroups/StatusCommandGroup.py | 18 +- sentio_prober_control/Sentio/Enumerations.py | 280 +++++++++--------- 2 files changed, 152 insertions(+), 146 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 326baa7..3dc8e16 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -1,5 +1,8 @@ from typing import Tuple, Union -from sentio_prober_control.Sentio.Enumerations import ThermoChuckState +from sentio_prober_control.Sentio.Enumerations import ( + ThermoChuckState, + DialogButtons +) from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ( ModuleCommandGroupBase, @@ -270,23 +273,23 @@ def set_prop(self, prop_name: str, stage: str, chuck_site: str = None) -> Respon self.comm.send(cmd) return Response.check_resp(self.comm.read_line()) - def show_message(self, message: str, button: str = "OK", caption: str = "None", level: str = "Hint") -> str: + def show_message(self, message: str, button: DialogButtons = DialogButtons.Ok, caption: str = "None", level: str = "Hint") -> str: """Show a message dialog for user interaction. Args: message: The message to show. - button: The button configuration (e.g., 'OK', 'OKCancel', 'YesNo'). + button: The button configuration (e.g., 'Ok', 'OkCancel', 'YesNo'). caption: Caption for the dialog. level: Level of the message (e.g., 'Hint', 'Warning', 'Error'). Returns: The button that was pressed. """ - self.comm.send(f"status:show_message {message},{button},{caption},{level}") + self.comm.send(f"status:show_message {message},{button.toSentioAbbr()},{caption},{level}") resp = Response.check_resp(self.comm.read_line()) return resp.message() - def start_show_message(self, message: str, button: str = "OK", caption: str = "None", level: str = "Hint") -> str: + def start_show_message(self, message: str, button: DialogButtons = DialogButtons.Ok, caption: str = "None", level: str = "Hint") -> Response: """Start an asynchronous message dialog. Args: @@ -298,6 +301,5 @@ def start_show_message(self, message: str, button: str = "OK", caption: str = "N Returns: A string containing Command ID and button pressed info. """ - self.comm.send(f"status:start_show_message {message},{button},{caption},{level}") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() \ No newline at end of file + self.comm.send(f"status:start_show_message {message},{button.toSentioAbbr()},{caption},{level}") + return Response.check_resp(self.comm.read_line()) diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 8267f96..f5af332 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -79,22 +79,6 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid auto focus function") -class MoveAxis(Enum): - """Defines the movement axis for the auto-focus function.""" - - Scope = 0 - Imagpro = 1 - Chuck = 2 - - def toSentioAbbr(self): - switcher = { - MoveAxis.Scope: "scope", - MoveAxis.Imagpro: "imagpro", - MoveAxis.Chuck: "chuck", - } - return switcher.get(self, "Invalid AxisOrient") - - class AxisOrient(Enum): """Represents an axis orientation. @@ -168,61 +152,6 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid bin quality identifier") -class PathSelection(Enum): - """An enumerator for defining the path selection state of a die. - - Attributes: - Pass (0): Die is marked as pass and valid for good bin selection. - Fail (1): Die is marked as fail and should be skipped or binned as failed. - Undefined (2): Die has no defined path selection; may be excluded from testing. - Unbinned (3): Die has not yet been assigned to any bin. - """ - - Pass = 0 - Fail = 1 - Undefined = 2 - Unbinned = 3 - - def toSentioAbbr(self): - switcher = { - PathSelection.Pass: "pass", - PathSelection.Fail: "fail", - PathSelection.Undefined: "undefined", - PathSelection.Unbinned: "unbinned", - } - return switcher.get(self, "Invalid path selection identifier") - - -class SubsiteGroup(Enum): - """An enumerator for defining subsite group types for get_num() command. - - Attributes: - Present (0): Subsites present on a specific die. - Selected (1): Subsites selected for routing on a specific die. - GlobalPresent (2): Global subsite table entries. - GlobalSelected (3): Globally selected subsites. - WaferPresent (4): Subsites present on the entire wafer. - WaferSelected (5): Selected subsites on the entire wafer. - """ - Present = 0 - Selected = 1 - GlobalPresent = 2 - GlobalSelected = 3 - WaferPresent = 4 - WaferSelected = 5 - - def toSentioAbbr(self) -> str: - switcher = { - SubsiteGroup.Present: "P", - SubsiteGroup.Selected: "S", - SubsiteGroup.GlobalPresent: "GP", - SubsiteGroup.GlobalSelected: "GS", - SubsiteGroup.WaferPresent: "WP", - SubsiteGroup.WaferSelected: "WS", - } - return switcher.get(self, "Invalid subsite group identifier") - - class CameraMountPoint(Enum): """Available camera mount points. @@ -347,6 +276,40 @@ def fromSentioAbbr(abbr: str) -> "ChuckSite": except KeyError: raise ValueError(f"Unknown ChuckSite abbreviation: {abbr}") +class ChuckThermoEnergyMode(Enum): + Fast = 0 + Optimal = 1 + HighPower = 2 + Customized = 3 + + def toSentioAbbr(self): + switcher = { + ChuckThermoEnergyMode.Fast: "Fast", + ChuckThermoEnergyMode.Optimal: "Optimal", + ChuckThermoEnergyMode.HighPower: "HighPower", + ChuckThermoEnergyMode.Customized: "Customized", + } + return switcher.get(self, "Invalid ChuckThermoEnergyMode") + + +class ChuckThermoHoldMode(Enum): + """An enumeration containing chuck thermo hold mode. + + Attributes: + Active (0): Chuck is holding temperature. + Nonactive (1): Chuck is not actively controlling temperature. + """ + + Active = 0 + Nonactive = 1 + + def toSentioAbbr(self): + switcher = { + ChuckThermoHoldMode.Active: "Active", + ChuckThermoHoldMode.Nonactive: "Nonactive", + } + return switcher.get(self, "Invalid ChuckThermoHoldMode") + class ChuckThetaReference(Enum): """Reference to use for chuck theat motions. @@ -426,6 +389,28 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid chuck z reference") + +class ChuckSpeed(Enum): + Fast = 0 + Normal = 1 + Slow = 2 + Jog = 3 + Index = 4 + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Fast": ChuckSpeed.Fast, + "Normal": ChuckSpeed.Normal, + "Slow": ChuckSpeed.Slow, + "Jog": ChuckSpeed.Jog, + "Index": ChuckSpeed.Index, + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckSpeed abbreviation: {abbr}") + class ColorScheme(Enum): """The color scheme used by the wafer map. @@ -529,7 +514,6 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid CompensationType") - class XyCompensationType(Enum): """A list of XY compensation types. @@ -705,6 +689,23 @@ def toSentioAbbr(self): DialogButtons.YesNoCancel: "YesNoCancel", } return switcher.get(self, "Invalid button id") + + @staticmethod + def fromSentioAbbr(abbr: str) -> "DialogButtons": + mapping = { + "Ok": DialogButtons.Ok, + "Cancel": DialogButtons.Cancel, + "OkCancel": DialogButtons.OkCancel, + "Yes": DialogButtons.Yes, + "No": DialogButtons.No, + "YesNo": DialogButtons.YesNo, + "YesCancel": DialogButtons.YesCancel, + "YesNoCancel": DialogButtons.YesNoCancel, + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown button abbreviation: {abbr}") @deprecated("Use CompensationMode instead") @@ -985,7 +986,21 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid Module Name") +class MoveAxis(Enum): + """Defines the movement axis for the auto-focus function.""" + + Scope = 0 + Imagpro = 1 + Chuck = 2 + def toSentioAbbr(self): + switcher = { + MoveAxis.Scope: "scope", + MoveAxis.Imagpro: "imagpro", + MoveAxis.Chuck: "chuck", + } + return switcher.get(self, "Invalid AxisOrient") + @deprecated(reason="duplicated; Use CompensationMode instead.") class OnTheFlyMode(Enum): Lateral = 0 @@ -1026,6 +1041,31 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid orientation marker") +class PathSelection(Enum): + """An enumerator for defining the path selection state of a die. + + Attributes: + Pass (0): Die is marked as pass and valid for good bin selection. + Fail (1): Die is marked as fail and should be skipped or binned as failed. + Undefined (2): Die has no defined path selection; may be excluded from testing. + Unbinned (3): Die has not yet been assigned to any bin. + """ + + Pass = 0 + Fail = 1 + Undefined = 2 + Unbinned = 3 + + def toSentioAbbr(self): + switcher = { + PathSelection.Pass: "pass", + PathSelection.Fail: "fail", + PathSelection.Undefined: "undefined", + PathSelection.Unbinned: "unbinned", + } + return switcher.get(self, "Invalid path selection identifier") + + class PoiReferenceXy(Enum): """Referenc position for points of interest. @@ -1633,6 +1673,36 @@ def toSentioArg(self): return switcher.get(self, "Invalid SoftwareFence parameter") +class SubsiteGroup(Enum): + """An enumerator for defining subsite group types for get_num() command. + + Attributes: + Present (0): Subsites present on a specific die. + Selected (1): Subsites selected for routing on a specific die. + GlobalPresent (2): Global subsite table entries. + GlobalSelected (3): Globally selected subsites. + WaferPresent (4): Subsites present on the entire wafer. + WaferSelected (5): Selected subsites on the entire wafer. + """ + Present = 0 + Selected = 1 + GlobalPresent = 2 + GlobalSelected = 3 + WaferPresent = 4 + WaferSelected = 5 + + def toSentioAbbr(self) -> str: + switcher = { + SubsiteGroup.Present: "P", + SubsiteGroup.Selected: "S", + SubsiteGroup.GlobalPresent: "GP", + SubsiteGroup.GlobalSelected: "GS", + SubsiteGroup.WaferPresent: "WP", + SubsiteGroup.WaferSelected: "WS", + } + return switcher.get(self, "Invalid subsite group identifier") + + class ThermoChuckState(Enum): """The state of a thermo chuck. @@ -1825,25 +1895,6 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid ZPositionHint") - """ - 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. @@ -1865,53 +1916,6 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid fiber type enumerator") -class ChuckThermoEnergyMode(Enum): - Fast = 0 - Optimal = 1 - HighPower = 2 - Customized = 3 - - def toSentioAbbr(self): - switcher = { - ChuckThermoEnergyMode.Fast: "Fast", - ChuckThermoEnergyMode.Optimal: "Optimal", - ChuckThermoEnergyMode.HighPower: "HighPower", - ChuckThermoEnergyMode.Customized: "Customized", - } - return switcher.get(self, "Invalid ChuckThermoEnergyMode") - -class ChuckThermoHoldMode(Enum): - Active = 0 - Nonactive = 1 - - def toSentioAbbr(self): - switcher = { - ChuckThermoHoldMode.Active: "Active", - ChuckThermoHoldMode.Nonactive: "Nonactive", - } - return switcher.get(self, "Invalid ChuckThermoHoldMode") - -class ChuckSpeed(Enum): - Fast = 0 - Normal = 1 - Slow = 2 - Jog = 3 - Index = 4 - - @staticmethod - def fromSentioAbbr(abbr: str): - mapping = { - "Fast": ChuckSpeed.Fast, - "Normal": ChuckSpeed.Normal, - "Slow": ChuckSpeed.Slow, - "Jog": ChuckSpeed.Jog, - "Index": ChuckSpeed.Index, - } - try: - return mapping[abbr] - except KeyError: - raise ValueError(f"Unknown ChuckSpeed abbreviation: {abbr}") - class VacuumState(Enum): Off = 0 On = 1 From 14df252d7251ae763f6b8fade100e0b93dc5de60 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 12:35:25 +0200 Subject: [PATCH 35/41] work on code review issues removed response return values get_access_level return value set to enumeration --- .../CommandGroups/StatusCommandGroup.py | 34 ++++++++++--------- sentio_prober_control/Sentio/Enumerations.py | 27 ++++++++++++++- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 3dc8e16..8f13997 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -1,5 +1,6 @@ -from typing import Tuple, Union +from typing import Tuple from sentio_prober_control.Sentio.Enumerations import ( + AccessLevel, ThermoChuckState, DialogButtons ) @@ -153,61 +154,62 @@ def get_high_purge_state(self) -> str: resp = Response.check_resp(self.comm.read_line()) return resp.message() - def set_chuck_thermo_energy_mode(self, mode: str) -> Response: + def set_chuck_thermo_energy_mode(self, mode: str) -> None: """Set chuck thermo energy mode. Args: mode: The desired energy mode. Possible values: Fast, Optimal, HighPower, Customized. Returns: - A Response object confirming the command execution. + None Raises: ValueError: If the provided mode is not valid. """ self.comm.send(f"status:set_chuck_thermo_energy_mode {mode}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def set_chuck_thermo_hold_mode(self, mode: bool) -> Response: + def set_chuck_thermo_hold_mode(self, mode: bool) -> None: """Set thermo chuck hold mode. Args: mode: A boolean indicating whether to enable (True) or disable (False) hold mode. Returns: - A Response object confirming the command execution. + None """ self.comm.send(f"status:set_chuck_thermo_hold_mode {mode}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def set_chuck_thermo_mode(self, mode: str) -> Response: + def set_chuck_thermo_mode(self, mode: str) -> None: """Set chuck thermo operation mode. Args: mode: The operation mode to set. Possible values: Normal, Standby, Defrost, Purge, Turbo, Eco. Returns: - A Response object confirming the command execution. + None Raises: ValueError: If the provided mode is not valid. """ self.comm.send(f"status:set_chuck_thermo_mode {mode}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def set_high_purge(self, enable: bool) -> Response: + def set_high_purge(self, enable: bool) -> None: """Set thermo chuck high purge state. Args: enable: A boolean indicating whether to enable (True) or disable (False) high purge + Returns: - A Response object confirming the command execution. + None """ self.comm.send(f"status:set_high_purge {enable}") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) - def get_access_level(self) -> str: + def get_access_level(self) -> AccessLevel: """Retrieves the access level of operation. Returns: @@ -215,8 +217,8 @@ def get_access_level(self) -> str: 'Operator', 'Admin', 'Service', 'Engineer', 'Debug'. """ self.comm.send("status:get_access_level") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + level : AccessLevel = AccessLevel[Response.check_resp(self.comm.read_line()).message()] + return level def get_prop(self, prop_name: str, stage: str = None) -> str: """Retrieve a specific data item from the setup module. diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index f5af332..6d703d6 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -2,6 +2,31 @@ from deprecated import deprecated +class AccessLevel(Enum): + """Specifies a SENTIO access level. + + Attributes: + Operator (1): Operator access + Admin (2): Admin access + Engineer (4): Engineer access + Service (8): Service access + Debug (16): Debug access + """ + Operator = 1 << 0, + Admin = 1 << 1, + Engineer = 1 << 2, + Service = 1 << 3, + Debug = 1 << 4 + + def toSentioAbbr(self): + switcher = { + AccessLevel.Operator: "Operator", + AccessLevel.Admin: "Admin", + AccessLevel.Engineer: "Engineer", + AccessLevel.Service: "Service", + AccessLevel.Debug: "Debug", + } + return switcher.get(self, "Invalid Auto Align function") class AutoAlignCmd(Enum): """Specifies an algorithm for performaing auto alignment. @@ -1702,7 +1727,7 @@ def toSentioAbbr(self) -> str: } return switcher.get(self, "Invalid subsite group identifier") - + class ThermoChuckState(Enum): """The state of a thermo chuck. From 632b497fb2f60ae7ed36b595754cdc9f34f3bf0f Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 12:39:19 +0200 Subject: [PATCH 36/41] deprecatedmarker braught back --- .../Sentio/CommandGroups/WafermapCommandGroup.py | 3 ++- .../Sentio/CommandGroups/WafermapCompensationCommandGroup.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py index adc12a6..71a27c1 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py @@ -1,5 +1,5 @@ from typing import Tuple - +from deprecated import deprecated from sentio_prober_control.Sentio.Enumerations import AxisOrient, ColorScheme, DieNumber, StatusBits, RoutingStartPoint, \ RoutingPriority, OrientationMarker from sentio_prober_control.Sentio.ProberBase import ProberException @@ -14,6 +14,7 @@ from sentio_prober_control.Sentio.CommandGroups.WafermapViewCommandGroup import WafermapViewCommandGroup +@deprecated("Use VisionCompensationGroup instead") class WafermapCommandGroup(ModuleCommandGroupBase): """This class represents the SENTIO command group for wafermap related commands. You are not meant to instantiate objects of this class directly! This class diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py index 808a595..a873a89 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCompensationCommandGroup.py @@ -5,9 +5,11 @@ from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase +@deprecated("Use VisionCompensationGroup instead") class WafermapCompensationCommandGroup(CommandGroupBase): """This command group bundles functions for setting up and using XY/Z compensation on the wafermap.""" + @deprecated("Use vision.compensation.enable instead") def topography(self, execute: ExecuteAction) -> Response: """Execute topography compensation. From 6f84fd081f5c840b12374ed9212389c82b663d72 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 13:51:03 +0200 Subject: [PATCH 37/41] cleanup status command group: set_prop and get_prop removed --- .../CommandGroups/ModuleCommandGroupBase.py | 9 +++-- .../CommandGroups/StatusCommandGroup.py | 35 ------------------- .../CommandGroups/WafermapCommandGroup.py | 1 - 3 files changed, 7 insertions(+), 38 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/ModuleCommandGroupBase.py b/sentio_prober_control/Sentio/CommandGroups/ModuleCommandGroupBase.py index 1021d18..93c29f1 100644 --- a/sentio_prober_control/Sentio/CommandGroups/ModuleCommandGroupBase.py +++ b/sentio_prober_control/Sentio/CommandGroups/ModuleCommandGroupBase.py @@ -1,7 +1,7 @@ from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase from typing import Tuple - +from enum import Enum class ModuleCommandGroupBase(CommandGroupBase): """Base class for all command groups.""" @@ -30,6 +30,8 @@ def get_prop(self, prop_name: str, arg1=None) -> int | float | str | bool | Tupl if arg1 == None: self.comm.send(f"{self._groupAbbr}:get_prop {prop_name}") + elif isinstance(arg1, Enum): + self.comm.send(f"{self._groupAbbr}:get_prop {prop_name}, {arg1.name}") else: self.comm.send(f"{self._groupAbbr}:get_prop {prop_name}, {arg1}") @@ -78,7 +80,10 @@ def set_prop(self, prop_name: str, *argv) -> None: """ cmd: str = self._groupAbbr + ":set_prop {0}" for n in range(0, len(argv)): - cmd += ", {0}".format(argv[n]) + if isinstance(argv[n], Enum): + cmd += f", {argv[n].name}" + else: + cmd += f", {argv[n]}" self.comm.send(cmd.format(prop_name)) Response.check_resp(self.comm.read_line()) diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 8f13997..54283f5 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -220,24 +220,6 @@ def get_access_level(self) -> AccessLevel: level : AccessLevel = AccessLevel[Response.check_resp(self.comm.read_line()).message()] return level - def get_prop(self, prop_name: str, stage: str = None) -> str: - """Retrieve a specific data item from the setup module. - - Args: - prop_name: The property name to query. Valid examples include: - 'Edge_Sensor', 'Z_Position_Hint', 'Active_Stage'. - stage: Optional stage name for 'Z_Position_Hint'. Can be 'Chuck', 'Scope', 'Probe01' ~ 'Probe04'. - - Returns: - A string value representing the queried property. - """ - cmd = f"status:get_prop {prop_name}" - if stage: - cmd += f" {stage}" - self.comm.send(cmd) - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - def get_machine_id(self) -> str: """Retrieves the machine ID. @@ -258,23 +240,6 @@ def get_version(self) -> str: resp = Response.check_resp(self.comm.read_line()) return resp.message() - def set_prop(self, prop_name: str, stage: str, chuck_site: str = None) -> Response: - """Set a specific property in the dashboard module. - - Args: - prop_name: The property name (e.g., 'Active_Stage'). - stage: The stage to apply the setting (e.g., 'Chuck', 'Scope'). - chuck_site: Optional. Only used when stage is 'Chuck' (e.g., 'Wafer', 'Auxright1'). - - Returns: - A Response object after setting the property. - """ - cmd = f"status:set_prop {prop_name} {stage}" - if chuck_site: - cmd += f", {chuck_site}" - self.comm.send(cmd) - return Response.check_resp(self.comm.read_line()) - def show_message(self, message: str, button: DialogButtons = DialogButtons.Ok, caption: str = "None", level: str = "Hint") -> str: """Show a message dialog for user interaction. diff --git a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py index 71a27c1..805d123 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapCommandGroup.py @@ -14,7 +14,6 @@ from sentio_prober_control.Sentio.CommandGroups.WafermapViewCommandGroup import WafermapViewCommandGroup -@deprecated("Use VisionCompensationGroup instead") class WafermapCommandGroup(ModuleCommandGroupBase): """This class represents the SENTIO command group for wafermap related commands. You are not meant to instantiate objects of this class directly! This class From da89490101dae3ad9f83cc6f174d115fe382924e Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 14:07:10 +0200 Subject: [PATCH 38/41] Update Enumerations.py enums ordered alphabetically; we need this for the documentation --- sentio_prober_control/Sentio/Enumerations.py | 230 +++++++++---------- 1 file changed, 108 insertions(+), 122 deletions(-) diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 6d703d6..8d64023 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -301,6 +301,29 @@ def fromSentioAbbr(abbr: str) -> "ChuckSite": except KeyError: raise ValueError(f"Unknown ChuckSite abbreviation: {abbr}") + +class ChuckSpeed(Enum): + Fast = 0 + Normal = 1 + Slow = 2 + Jog = 3 + Index = 4 + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "Fast": ChuckSpeed.Fast, + "Normal": ChuckSpeed.Normal, + "Slow": ChuckSpeed.Slow, + "Jog": ChuckSpeed.Jog, + "Index": ChuckSpeed.Index, + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown ChuckSpeed abbreviation: {abbr}") + + class ChuckThermoEnergyMode(Enum): Fast = 0 Optimal = 1 @@ -414,28 +437,6 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid chuck z reference") - -class ChuckSpeed(Enum): - Fast = 0 - Normal = 1 - Slow = 2 - Jog = 3 - Index = 4 - - @staticmethod - def fromSentioAbbr(abbr: str): - mapping = { - "Fast": ChuckSpeed.Fast, - "Normal": ChuckSpeed.Normal, - "Slow": ChuckSpeed.Slow, - "Jog": ChuckSpeed.Jog, - "Index": ChuckSpeed.Index, - } - try: - return mapping[abbr] - except KeyError: - raise ValueError(f"Unknown ChuckSpeed abbreviation: {abbr}") - class ColorScheme(Enum): """The color scheme used by the wafer map. @@ -539,31 +540,6 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid CompensationType") -class XyCompensationType(Enum): - """A list of XY compensation types. - - Attributes: - Disable (0): None - Topography (1): Vertical (Z) compensation. - MapScan (2): Both lateral and vertical compensation. - AlignDie (3): Probe card compensation. - SkateDetection (4): MapScan compensation. - """ - - Disable = 0 - OnTheFly = 1 - MapScan = 2 - Thermal = 3 - - def toSentioAbbr(self): - switcher = { - XyCompensationType.Disable: "None", - XyCompensationType.OnTheFly: "OnTheFly", - XyCompensationType.MapScan: "MapScan", - XyCompensationType.Thermal: "Thermal", - } - return switcher.get(self, "Invalid XyCompensationType") - class ZCompensationType(Enum): """A list of Z compensation types. @@ -855,7 +831,7 @@ def toSentioAbbr(self): FiberType.Lensed: "Lensed", } return switcher.get(self, "Invalid fiber type enumerator") - + class FindPatternReference(Enum): """Reference point for coordinates than returning a pattern position. @@ -876,6 +852,17 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid find pattern reference id") +class HighPowerAirState(Enum): + Off = 0 + On = 1 + + def toSentioAbbr(self) -> str: + return { + HighPowerAirState.Off: "0", + HighPowerAirState.On: "1", + }.get(self, "Invalid HighPowerAirState") + + class ImagePattern(Enum): align = 0 home = 1 @@ -1011,6 +998,7 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid Module Name") + class MoveAxis(Enum): """Defines the movement axis for the auto-focus function.""" @@ -1176,6 +1164,17 @@ class SnapshotLocation(Enum): This option only makes sense if the prober and the control PC are different. """ +class SoftContactState(Enum): + Disable = 0 + Enable = 1 + + def toSentioAbbr(self) -> str: + return { + SoftContactState.Disable: "0", + SoftContactState.Enable: "1", + }.get(self, "Invalid SoftContactState") + + class Stage(Enum): """Represents a stage in SENTIO. @@ -1750,6 +1749,18 @@ class ThermoChuckState(Enum): Controlling = 6 Unknown = 7 + +class UserCoordState(Enum): + Chuck = 0 + Scope = 1 + + def toSentioAbbr(self) -> str: + return { + UserCoordState.Chuck: "chuck", + UserCoordState.Scope: "scope" + }.get(self, "Invalid UserCoordState") + + class UvwAxis(Enum): """An enumeration containing UVW axis. @@ -1772,6 +1783,28 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid UVW enumerator") +class VacuumState(Enum): + Off = 0 + On = 1 + + def toSentioAbbr(self) -> str: + return { + VacuumState.Off: "Off", + VacuumState.On: "On", + }.get(self, "Invalid VacuumState") + + @staticmethod + def fromSentioAbbr(abbr: str): + mapping = { + "0": VacuumState.Off, + "1": VacuumState.On + } + try: + return mapping[abbr] + except KeyError: + raise ValueError(f"Unknown VacuumState abbreviation: {abbr}") + + class VceZReference(Enum): """Reference for Vce z motions. @@ -1886,6 +1919,32 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid chuck site") +class XyCompensationType(Enum): + """A list of XY compensation types. + + Attributes: + Disable (0): None + Topography (1): Vertical (Z) compensation. + MapScan (2): Both lateral and vertical compensation. + AlignDie (3): Probe card compensation. + SkateDetection (4): MapScan compensation. + """ + + Disable = 0 + OnTheFly = 1 + MapScan = 2 + Thermal = 3 + + def toSentioAbbr(self): + switcher = { + XyCompensationType.Disable: "None", + XyCompensationType.OnTheFly: "OnTheFly", + XyCompensationType.MapScan: "MapScan", + XyCompensationType.Thermal: "Thermal", + } + return switcher.get(self, "Invalid XyCompensationType") + + class ZPositionHint(Enum): """Represents a hint for the z position of a stage. @@ -1917,77 +1976,4 @@ def toSentioAbbr(self): ZPositionHint.Lift: "Lift", ZPositionHint.Transfer: "Transfer", } - return switcher.get(self, "Invalid ZPositionHint") - - -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") - -class VacuumState(Enum): - Off = 0 - On = 1 - - def toSentioAbbr(self) -> str: - return { - VacuumState.Off: "Off", - VacuumState.On: "On", - }.get(self, "Invalid VacuumState") - - @staticmethod - def fromSentioAbbr(abbr: str): - mapping = { - "0": VacuumState.Off, - "1": VacuumState.On - } - try: - return mapping[abbr] - except KeyError: - raise ValueError(f"Unknown VacuumState abbreviation: {abbr}") - -class HighPowerAirState(Enum): - Off = 0 - On = 1 - - def toSentioAbbr(self) -> str: - return { - HighPowerAirState.Off: "0", - HighPowerAirState.On: "1", - }.get(self, "Invalid HighPowerAirState") - -class SoftContactState(Enum): - Disable = 0 - Enable = 1 - - def toSentioAbbr(self) -> str: - return { - SoftContactState.Disable: "0", - SoftContactState.Enable: "1", - }.get(self, "Invalid SoftContactState") - -class UserCoordState(Enum): - Chuck = 0 - Scope = 1 - - def toSentioAbbr(self) -> str: - return { - UserCoordState.Chuck: "chuck", - UserCoordState.Scope: "scope" - }.get(self, "Invalid UserCoordState") + return switcher.get(self, "Invalid ZPositionHint") \ No newline at end of file From 00a3a1577bc0aca04db639ef8ca8ee0514facb0a Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 14:11:55 +0200 Subject: [PATCH 39/41] member functions sorted alphabetically; needed for pydoc documentation --- .../CommandGroups/StatusCommandGroup.py | 126 ++++++++++-------- sentio_prober_control/Sentio/Enumerations.py | 1 + 2 files changed, 68 insertions(+), 59 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 54283f5..86515e5 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -15,6 +15,17 @@ class StatusCommandGroup(ModuleCommandGroupBase): def __init__(self, comm) -> None: super().__init__(comm, "status") + def get_access_level(self) -> AccessLevel: + """Retrieves the access level of operation. + + Returns: + A string representing the access level. Possible values are: + 'Operator', 'Admin', 'Service', 'Engineer', 'Debug'. + """ + self.comm.send("status:get_access_level") + level : AccessLevel = AccessLevel[Response.check_resp(self.comm.read_line()).message()] + return level + def get_chuck_temp(self) -> float: """Get current chuck temperature. @@ -44,6 +55,27 @@ def get_chuck_temp_setpoint(self) -> float: return temp + def get_chuck_thermo_energy_mode(self) -> str: + """Get the current chuck thermo energy mode. + Returns: + The current energy mode as a string. Possible values: Fast, Optimal, HighPower, Customized. + """ + self.comm.send("status:get_chuck_thermo_energy_mode") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + + def get_chuck_thermo_hold_mode(self) -> str: + """Get thermo chuck hold mode. + + Returns: + The current hold mode. Possible values: Active, Nonactive. + """ + self.comm.send("status:get_chuck_thermo_hold_mode") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def get_chuck_thermo_state(self) -> ThermoChuckState: """Return thermo chuck state. @@ -73,6 +105,28 @@ def get_chuck_thermo_state(self) -> ThermoChuckState: return ThermoChuckState.Unknown + def get_high_purge_state(self) -> str: + """Get thermo chuck high purge state. + + Returns: + The current high purge state. Possible values: ON, OFF. + """ + self.comm.send("status:get_high_purge_state") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + + def get_machine_id(self) -> str: + """Retrieves the machine ID. + + Returns: + A string containing the machine ID. + """ + self.comm.send("status:get_machine_id") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def get_machine_status(self) -> Tuple[bool, bool, bool]: """Get machine status. @@ -110,6 +164,17 @@ def get_soaking_time(self, temperature: float): return temp + def get_version(self) -> str: + """Retrieves the system version. + + Returns: + A string containing version information. + """ + self.comm.send("status:get_version") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def set_chuck_temp(self, temp: float, lift_chuck : bool = False) -> None: """Set chuck temperature setpoint. @@ -124,35 +189,7 @@ def set_chuck_temp(self, temp: float, lift_chuck : bool = False) -> None: """ self.comm.send(f"status:set_chuck_temp {temp:.2f}, {lift_chuck}") Response.check_resp(self.comm.read_line()) - - def get_chuck_thermo_energy_mode(self) -> str: - """Get the current chuck thermo energy mode. - Returns: - The current energy mode as a string. Possible values: Fast, Optimal, HighPower, Customized. - """ - self.comm.send("status:get_chuck_thermo_energy_mode") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - - def get_chuck_thermo_hold_mode(self) -> str: - """Get thermo chuck hold mode. - - Returns: - The current hold mode. Possible values: Active, Nonactive. - """ - self.comm.send("status:get_chuck_thermo_hold_mode") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - - def get_high_purge_state(self) -> str: - """Get thermo chuck high purge state. - - Returns: - The current high purge state. Possible values: ON, OFF. - """ - self.comm.send("status:get_high_purge_state") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() + def set_chuck_thermo_energy_mode(self, mode: str) -> None: """Set chuck thermo energy mode. @@ -208,37 +245,7 @@ def set_high_purge(self, enable: bool) -> None: self.comm.send(f"status:set_high_purge {enable}") Response.check_resp(self.comm.read_line()) - - def get_access_level(self) -> AccessLevel: - """Retrieves the access level of operation. - - Returns: - A string representing the access level. Possible values are: - 'Operator', 'Admin', 'Service', 'Engineer', 'Debug'. - """ - self.comm.send("status:get_access_level") - level : AccessLevel = AccessLevel[Response.check_resp(self.comm.read_line()).message()] - return level - def get_machine_id(self) -> str: - """Retrieves the machine ID. - - Returns: - A string containing the machine ID. - """ - self.comm.send("status:get_machine_id") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - - def get_version(self) -> str: - """Retrieves the system version. - - Returns: - A string containing version information. - """ - self.comm.send("status:get_version") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() def show_message(self, message: str, button: DialogButtons = DialogButtons.Ok, caption: str = "None", level: str = "Hint") -> str: """Show a message dialog for user interaction. @@ -256,6 +263,7 @@ def show_message(self, message: str, button: DialogButtons = DialogButtons.Ok, c resp = Response.check_resp(self.comm.read_line()) return resp.message() + def start_show_message(self, message: str, button: DialogButtons = DialogButtons.Ok, caption: str = "None", level: str = "Hint") -> Response: """Start an asynchronous message dialog. diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 8d64023..b9e791d 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -28,6 +28,7 @@ def toSentioAbbr(self): } return switcher.get(self, "Invalid Auto Align function") + class AutoAlignCmd(Enum): """Specifies an algorithm for performaing auto alignment. From 9c3db7d51928389498e09657a03f6908f0c1384b Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 14:25:21 +0200 Subject: [PATCH 40/41] Update QAlibriaCommandGroup.py --- .../Sentio/CommandGroups/QAlibriaCommandGroup.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py index 9271785..95aaaad 100644 --- a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py @@ -2,12 +2,8 @@ from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase from sentio_prober_control.Sentio.ProberBase import ProberException +from sentio_prober_control.Sentio.Enumerations import DriftType from typing import List -from enum import Enum - -class DriftType(Enum): - DriftRef = "DriftRef" - Drift = "Drift" class QAlibriaCommandGroup(CommandGroupBase): """ @@ -98,12 +94,7 @@ def check_calibration_status(self) -> None: ProberException: If the calibration status is not "OK". """ self.comm.send("qal:get_calibration_status") - resp = Response.check_resp(self.comm.read_line()) - status = resp.message().strip() - if status.upper() != "OK": - raise ProberException(f"Calibration status error: {status}") - - + Response.check_resp(self.comm.read_line()) def measurement_execute( self, From 5c3362731ac2d512f16bf315d251749bb1dcc5a5 Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Thu, 8 May 2025 14:30:13 +0200 Subject: [PATCH 41/41] methods reordered alphabetically --- .../CommandGroups/QAlibriaCommandGroup.py | 142 ++++++++---------- 1 file changed, 66 insertions(+), 76 deletions(-) diff --git a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py index 95aaaad..df9b808 100644 --- a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py @@ -31,69 +31,43 @@ def calibration_drift_verify(self, dut_name: str = "", auto_exec: bool = True) - Wraps SENTIO's "qal:calibration_drift_verify" remote command. Args: - dut_name: The name of the DUT. - auto_exec: Whether to automatically execute the drift verify process. + dut_name (str): The name of the DUT. + auto_exec (bool): Whether to automatically execute the drift verify process. """ self.comm.send(f"qal:calibration_drift_verify {dut_name},{str(auto_exec).lower()}") Response.check_resp(self.comm.read_line()) - - @deprecated(reason="use calibration_execute instead!; violates naming conventions. (CR#13887)") - def start_calibration(self) -> None: - """ - Deprecated function for starting calibration. - Please use calibration_execute instead. - """ - self.comm.send("qal:calibration_execute") - Response.check_resp(self.comm.read_line()) - - - @deprecated(reason="use calibration_drift_verify instead!; violates naming conventions. (CR#13887)") - def verify_calibration_drift(self) -> None: - """ - Deprecated function for verifying calibration drift. - Please use calibration_drift_verify instead. + def check_calibration_status(self) -> None: """ - self.comm.send("qal:calibration_drift_verify") - Response.check_resp(self.comm.read_line()) + Checks the calibration status of QAlibria. + Wraps SENTIO's "qal:get_calibration_status" remote command. - @deprecated(reason="use calibration_drift_verify instead!; violates naming conventions. (CR#13887)") - def verify_calibration_drift_dut(self, dut) -> None: - """ - Deprecated function for verifying calibration drift with a specific DUT. - Please use calibration_drift_verify(dut_name, ...) instead. + Raises: + ProberException: If the calibration status is not "OK". """ - self.comm.send(f"qal:calibration_drift_verify {dut}") + self.comm.send("qal:get_calibration_status") Response.check_resp(self.comm.read_line()) - - @deprecated(reason="oddly specific function name; filed as CR#13887") - def set_calibration_drift_probe12(self): - """ - Deprecated function for setting calibration drift for probe 1 and 2. + def clear_dut_network(self, dut_name: str, drift_type: DriftType, update_ui: bool) -> None: """ - self.comm.send("qal:set_dut_network RefDUT,DriftRef,12,false") - Response.check_resp(self.comm.read_line()) - - self.comm.send("qal:set_dut_network RefDUT,Drift,12,false") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - - # ------------------------------------------------------------------------- - # New / Additional Commands - # ------------------------------------------------------------------------- + Clear network data for a DUT. - def check_calibration_status(self) -> None: - """ - Checks the calibration status of QAlibria. + Wraps SENTIO's "qal:clear_dut_network" remote command. - Wraps SENTIO's "qal:get_calibration_status" remote command. + Args: + dut_name (str): The name of the DUT (e.g. "RefDUT"). + drift_type (DriftType): The type of drift data to clear (DriftType.DriftRef or DriftType.Drift). + update_ui (bool): Whether to update the UI (True/False). + Returns: + None + Raises: - ProberException: If the calibration status is not "OK". + ProberException: If the remote command returns an error. """ - self.comm.send("qal:get_calibration_status") + cmd = f"qal:clear_dut_network {dut_name},{drift_type.value},{str(update_ui).lower()}" + self.comm.send(cmd) Response.check_resp(self.comm.read_line()) def measurement_execute( @@ -139,6 +113,18 @@ def reset_ets(self) -> None: self.comm.send("qal:reset_ets") Response.check_resp(self.comm.read_line()) + @deprecated(reason="oddly specific function name; filed as CR#13887") + def set_calibration_drift_probe12(self): + """ + Deprecated function for setting calibration drift for probe 1 and 2. + """ + self.comm.send("qal:set_dut_network RefDUT,DriftRef,12,false") + Response.check_resp(self.comm.read_line()) + + self.comm.send("qal:set_dut_network RefDUT,Drift,12,false") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + def set_ets(self, port: int, path: str, ets_mode: int = 0) -> None: """ Set error terms in the buffer. @@ -171,42 +157,34 @@ def send_ets_to_vna(self, cal_set_name: str) -> None: self.comm.send(cmd) Response.check_resp(self.comm.read_line()) - def clear_dut_network(self, dut_name: str, drift_type: DriftType, update_ui: bool) -> None: + @deprecated(reason="use calibration_execute instead!; violates naming conventions. (CR#13887)") + def start_calibration(self) -> None: """ - Clear network data for a DUT. - - Wraps SENTIO's "qal:clear_dut_network" remote command. - - Args: - dut_name (str): The name of the DUT (e.g. "RefDUT"). - drift_type (DriftType): The type of drift data to clear (DriftType.DriftRef or DriftType.Drift). - update_ui (bool): Whether to update the UI (True/False). - - Returns: - None - - Raises: - ProberException: If the remote command returns an error. + Deprecated function for starting calibration. + Please use calibration_execute instead. """ - cmd = f"qal:clear_dut_network {dut_name},{drift_type.value},{str(update_ui).lower()}" - self.comm.send(cmd) + self.comm.send("qal:calibration_execute") Response.check_resp(self.comm.read_line()) - - - def vna_write(self, vna_command: str) -> None: + + @deprecated(reason="use calibration_drift_verify instead!; violates naming conventions. (CR#13887)") + def verify_calibration_drift(self) -> None: """ - Write a remote command to the VNA. + Deprecated function for verifying calibration drift. + Please use calibration_drift_verify instead. + """ + self.comm.send("qal:calibration_drift_verify") + Response.check_resp(self.comm.read_line()) - Wraps SENTIO's "qal:vna_write" remote command. - Args: - vna_command: The remote command to send to the VNA (e.g. ":SENS1:FREQ:STAR 1.0E9"). + @deprecated(reason="use calibration_drift_verify instead!; violates naming conventions. (CR#13887)") + def verify_calibration_drift_dut(self, dut) -> None: """ - cmd = f"qal:vna_write {vna_command}" - self.comm.send(cmd) + Deprecated function for verifying calibration drift with a specific DUT. + Please use calibration_drift_verify(dut_name, ...) instead. + """ + self.comm.send(f"qal:calibration_drift_verify {dut}") Response.check_resp(self.comm.read_line()) - def vna_query(self, vna_command: str) -> str: """ Query a remote command from the VNA and return the response. @@ -224,7 +202,6 @@ def vna_query(self, vna_command: str) -> str: resp = Response.check_resp(self.comm.read_line()) return resp.message() - def vna_read(self) -> str: """ Read data from the VNA. @@ -236,4 +213,17 @@ def vna_read(self) -> str: """ self.comm.send("qal:vna_read") resp = Response.check_resp(self.comm.read_line()) - return resp.message() \ No newline at end of file + return resp.message() + + def vna_write(self, vna_command: str) -> None: + """ + Write a remote command to the VNA. + + Wraps SENTIO's "qal:vna_write" remote command. + + Args: + vna_command: The remote command to send to the VNA (e.g. ":SENS1:FREQ:STAR 1.0E9"). + """ + cmd = f"qal:vna_write {vna_command}" + self.comm.send(cmd) + Response.check_resp(self.comm.read_line())