From b9773eca1f36c614e6f1b51a56e72284a89462cd Mon Sep 17 00:00:00 2001 From: Ingo Berg Date: Mon, 16 Jun 2025 15:32:56 +0200 Subject: [PATCH] Added support for SENTIO 25.2 generic stage commands Added support for SENTIO 25.2 generic stage commands --- .../Sentio/CommandGroups/ScopeCommandGroup.py | 58 ---- .../Sentio/CommandGroups/StageCommandGroup.py | 260 ++++++++++++++++++ sentio_prober_control/Sentio/Enumerations.py | 83 ++++-- sentio_prober_control/Sentio/ProberSentio.py | 5 +- 4 files changed, 320 insertions(+), 86 deletions(-) delete mode 100644 sentio_prober_control/Sentio/CommandGroups/ScopeCommandGroup.py create mode 100644 sentio_prober_control/Sentio/CommandGroups/StageCommandGroup.py diff --git a/sentio_prober_control/Sentio/CommandGroups/ScopeCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/ScopeCommandGroup.py deleted file mode 100644 index b827813..0000000 --- a/sentio_prober_control/Sentio/CommandGroups/ScopeCommandGroup.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Tuple - -from sentio_prober_control.Sentio.Enumerations import XyReference, Stage -from sentio_prober_control.Sentio.Response import Response -from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase - - -class ScopeCommandGroup(CommandGroupBase): - """This command group contains functions for working with motorized scopes. - You are not meant to instantiate this class directly. Access it via the probe attribute - of the [SentioProber](SentioProber.md) class. - - Example: - - ```py - from sentio_prober_control.Sentio.ProberSentio import SentioProber - - prober = SentioProber.create_prober("tcpip", "127.0.0.1:35555") - prober.scope.move_probe_xy(XyReference.Current, 1000, 2000) - ``` - """ - - def __init__(self, prober: 'SentioProber', stage : Stage, has_subgroups = False) -> None: # type: ignore - super().__init__(prober) - - self.__stage_selector: str = "" - - if stage==Stage.Scope: - self.__stage_selector = "top" - elif stage==Stage.BottomScope: - self.__stage_selector = "bottom" - elif stage==Stage.AuxiliaryScope: - self.__stage_selector = "aux" - else: - raise ValueError(f"Invalid stage {stage} for ScopeCommandGroup") - - if stage==Stage.Scope and has_subgroups: - self.top: ScopeCommandGroup = ScopeCommandGroup(prober, Stage.Scope) - self.bottom: ScopeCommandGroup = ScopeCommandGroup(prober, Stage.BottomScope) - self.aux: ScopeCommandGroup = ScopeCommandGroup(prober, Stage.AuxiliaryScope) - - - def move_xy(self, ref: XyReference, x: float, y: float) -> Tuple[float, float, XyReference]: - """Move scope to a given position. - - Args: - ref: The position reference for the submitted values. - x: The x position in micrometer. - y: The y position in micrometer. - - Returns: - A tuple containing the x and y position after the move. - """ - - self.comm.send(f"scope:{self.__stage_selector}:move_xy {ref.to_string()},{x},{y}") - resp = Response.check_resp(self.comm.read_line()) - tok = resp.message().split(",") - return float(tok[0]), float(tok[1]), XyReference.from_string(tok[2]) \ No newline at end of file diff --git a/sentio_prober_control/Sentio/CommandGroups/StageCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StageCommandGroup.py new file mode 100644 index 0000000..4a71283 --- /dev/null +++ b/sentio_prober_control/Sentio/CommandGroups/StageCommandGroup.py @@ -0,0 +1,260 @@ +from typing import Tuple + +from sentio_prober_control.Sentio.Enumerations import XyReference, ZReference, Stage, ChuckSite +from sentio_prober_control.Sentio.Response import Response +from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase + +from typing import Optional + +class StageCommandGroup(CommandGroupBase): + """This command group contains functions for working with motorized scopes. + You are not meant to instantiate this class directly. Access it via the probe attribute + of the [SentioProber](SentioProber.md) class. + + Example: + + ```py + from sentio_prober_control.Sentio.ProberSentio import SentioProber + + prober = SentioProber.create_prober("tcpip", "127.0.0.1:35555") + prober.scope.move_probe_xy(XyReference.Current, 1000, 2000) + ``` + """ + + def __init__(self, prober: 'SentioProber', stage : Stage, has_subgroups = False) -> None: # type: ignore + super().__init__(prober) + + self.__stage_selector: str = "" + self.__stage = stage + + if stage==Stage.Scope: + self.__stage_selector = "scope:top" + elif stage==Stage.BottomScope: + self.__stage_selector = "scope:bottom" + elif stage==Stage.AuxiliaryScope: + self.__stage_selector = "scope:aux" + elif stage==Stage.Chuck: + self.__stage_selector = "chuck" + else: + raise ValueError(f"Invalid stage {stage} for ScopeCommandGroup") + + if stage==Stage.Scope and has_subgroups: + self.top: StageCommandGroup = StageCommandGroup(prober, Stage.Scope, False) + self.bottom: StageCommandGroup = StageCommandGroup(prober, Stage.BottomScope, False) + self.aux: StageCommandGroup = StageCommandGroup(prober, Stage.AuxiliaryScope, False) + + + def get_home(self, chuck_site : ChuckSite = ChuckSite.Wafer) -> Tuple[float, float, ChuckSite]: + """Retrieves index coordinates of the home die. + + Returns: + Row and column index of the home die. + """ + self.comm.send(f"{self.__stage_selector}:get_home {chuck_site.to_string()}") + resp = Response.check_resp(self.comm.read_line()) + + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]), ChuckSite.from_string(tok[2]) + + + def get_site(self, site_idx : int) -> Tuple[str, float, float, XyReference, bool]: + """Returns the number of stage specific site position. + + Returns: + id (str): The site identifier. + x (float): The x position in micrometer. + y (float): The y position in micrometer. + ref (XyReference): The position reference for the returned values. + enabled (bool): True if the site is enabled, False otherwise. + """ + self.comm.send(f"{self.__stage_selector}:get_site {site_idx}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return tok[0],float(tok[1]), float(tok[2]), XyReference.from_string(tok[3]), bool(tok[4]) + + + def get_site_num(self) -> int: + """Returns the number of stage specific site position. + + Returns: + site_num (int): An integer number of sites for the stage. + """ + self.comm.send(f"{self.__stage_selector}:get_site_num") + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) + + + def get_xy(self, ref_xy : XyReference) -> Tuple[float, float, XyReference]: + """ Returns the stage position for a given reference. + + Returns: + x (float): The x position in micrometer. + y (float): The y position in micrometer. + ref (XyReference): The position reference for the returned values. + """ + self.comm.send(f"{self.__stage_selector}:get_xy {ref_xy.to_string()}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]), XyReference.from_string(tok[2]) + + + def get_z(self, ref_z : ZReference) -> Tuple[float, ZReference]: + """Returns stage z-position. + + Returns: + z (float): The x position in micrometer. + ref (XyReference): The position reference for the returned values. + """ + self.comm.send(f"{self.__stage_selector}:get_z {ref_z.to_string()}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), ZReference.from_string(tok[1]) + + + def has_xy(self) -> bool: + """Returns true if stage has an xy drive. + + Returns: + has_xy_drive (bool): True is the stage has a motorized xy drive. + """ + self.comm.send(f"{self.__stage_selector}:has_xy") + resp = Response.check_resp(self.comm.read_line()) + return bool(resp.message()) + + + def has_z(self) -> bool: + """Returns true if stage has an z drive. + + Returns: + has_z_drive (bool): True is the stage has a motorized z drive. + """ + self.comm.send(f"{self.__stage_selector}:has_z") + resp = Response.check_resp(self.comm.read_line()) + return bool(resp.message()) + + + def lift(self, lift : bool) -> None: + """Move stage to lift position. + + Returns: + None + """ + self.comm.send(f"{self.__stage_selector}:lift {lift}") + Response.check_resp(self.comm.read_line()) + + + def move_xy(self, ref: XyReference, x: float, y: float) -> Tuple[float, float, XyReference]: + """Move stage to a given position. + + Args: + ref: The position reference for the submitted values. + x: The x position in micrometer. + y: The y position in micrometer. + + Returns: + xpos (float): The x-position + ypos (float): The y-position + ref (XyReference): The position reference for the returned values. + """ + + self.comm.send(f"{self.__stage_selector}:move_xy {ref.to_string()},{x},{y}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), float(tok[1]), XyReference.from_string(tok[2]) + + + def move_z(self, ref: ZReference, z: float) -> Tuple[float, ZReference]: + """Move stage to a given z position. + + Args: + ref: The position reference for the submitted values. + z: The z position in micrometer. + + Returns: + zpos (float): The z-position + ref (ZReference): The position reference for the returned values. + """ + + self.comm.send(f"{self.__stage_selector}:move_z {ref.to_string()},{z}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]), ZReference.from_string(tok[1]) + + + def set_home(self, x: Optional[float] = None, y: Optional[float] = None, site: Optional[ChuckSite] = None) -> Tuple[float, float, ChuckSite]: + """ Set the stage home position. + + Args: + x (float): The x position in micrometer. + y (float): The y position in micrometer. + site (ChuckSite): The chuck site to set the home position for. + + Returns: + x (float): The x position in micrometer. + y (float): The y position in micrometer. + site (ChuckSite): The chuck site the home position was set for. + """ + if (x is None and y is not None) or (y is None and x is not None): + raise ValueError("If y is specified, x must also be specified (and vice versa).") + + args : str = f"{x},{y}" if x is not None and y is not None else "" + args += f" {site.to_string()}" if site is not None else "" + self.comm.send(f"{self.__stage_selector}:set_home {args}") + + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return float(tok[0]),float(tok[1]),ChuckSite.from_string(tok[2]) + + + def step_site(self, site: str | int) -> Tuple[str, float, float]: + """Step to a specific site. + + Args: + site_idx (str|int): The index or identifier of the site to step to. + + Returns: + id (str): The site identifier. + x (float): The x position in micrometer. + y (float): The y position in micrometer. + """ + + self.comm.send(f"{self.__stage_selector}:step_site {site}") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return tok[0],float(tok[1]),float(tok[2]) + + + def step_site_first(self) -> Tuple[str, float, float]: + """Step to the first site. + + Returns: + id (str): The site identifier. + x (float): The x position in micrometer. + y (float): The y position in micrometer. + """ + + self.comm.send(f"{self.__stage_selector}:step_site_first") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return tok[0],float(tok[1]),float(tok[2]) + + + def step_site_next(self) -> Tuple[str, float, float]: + """Step to the first site. + + Returns: + id (str): The site identifier. + x (float): The x position in micrometer. + y (float): The y position in micrometer. + """ + + self.comm.send(f"{self.__stage_selector}:step_site_next") + resp = Response.check_resp(self.comm.read_line()) + tok = resp.message().split(",") + return tok[0],float(tok[1]),float(tok[2]) + + + @property + def stage(self) -> Stage: + """The stage this command group is for.""" + return self.__stage \ No newline at end of file diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index aebfec6..106e453 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -1934,6 +1934,30 @@ def from_string(abbr: str) -> "XyReference": raise ValueError(f"Unknown XyReference abbreviation: {abbr}") +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 to_string(self): + switcher = { + ZCompensationType.Disable: "None", + ZCompensationType.OnTheFly: "OnTheFly", + ZCompensationType.Topography: "Topography", + } + return switcher.get(self, "Invalid XyCompensationType") + + class ZPositionHint(Enum): """Represents a hint for the z position of a stage. @@ -1984,30 +2008,6 @@ def from_string(abbr: str) -> "ZPositionHint": raise ValueError(f"Unknown ChuckPositionHint abbreviation: {abbr}") -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 to_string(self): - switcher = { - ZCompensationType.Disable: "None", - ZCompensationType.OnTheFly: "OnTheFly", - ZCompensationType.Topography: "Topography", - } - return switcher.get(self, "Invalid XyCompensationType") - - class ZReference(Enum): """Defines a position reference for stage z-motions. @@ -2017,6 +2017,10 @@ class ZReference(Enum): Hover (2): Use relative chuck z coordinated with respect to the chucks hover height. Zero (3): Use absolute chuck z coordinates with respect the the physical axis zero positon. Current (4): Use relative chuck z coordinated with respect to the current position. + Vce1 (5): For internal and debug use only + Vce2 (6): For internal and debug use only + Ready (7): For internal and debug use only + RealPos (8): For internal and debug use only """ Contact = 0 @@ -2024,13 +2028,40 @@ class ZReference(Enum): Hover = 2 Zero = 3 Current = 4 - + Vce1 = 5 + Vce2 = 6 + Ready = 7 + RealPos = 8 + def to_string(self): switcher = { ZReference.Contact: "C", ZReference.Separation: "S", ZReference.Hover: "H", ZReference.Zero: "Z", - ZReference.Current: "R" + ZReference.Current: "R", + ZReference.Vce1: "VCE01", + ZReference.Vce2: "VCE02", + ZReference.Ready: "Ready", + ZReference.RealPos: "RealPos", } return switcher.get(self, "Invalid chuck z reference") + + @staticmethod + def from_string(abbr: str) -> "ZReference": + """ Convert a string to a ZReference. """ + mapping = { + "contact" : ZReference.Contact, + "separation" : ZReference.Separation, + "hover": ZReference.Hover, + "zero": ZReference.Zero, + "current": ZReference.Current, + "vce01": ZReference.Vce1, + "vce02": ZReference.Vce2, + "ready": ZReference.Ready, + "realpos": ZReference.RealPos, + } + try: + return mapping[abbr.lower()] + except KeyError: + raise ValueError(f"Unknown ZReference abbreviation: {abbr}") diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 0131566..62a3a0b 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -44,7 +44,7 @@ from sentio_prober_control.Sentio.CommandGroups.VisionCommandGroup import VisionCommandGroup from sentio_prober_control.Sentio.CommandGroups.WafermapCommandGroup import WafermapCommandGroup from sentio_prober_control.Sentio.CommandGroups.SetupCommandGroup import SetupCommandGroup -from sentio_prober_control.Sentio.CommandGroups.ScopeCommandGroup import ScopeCommandGroup +from sentio_prober_control.Sentio.CommandGroups.StageCommandGroup import StageCommandGroup class SentioCommunicationType(Enum): """This enum defines different types of prober communication. @@ -102,7 +102,8 @@ def __init__(self, comm: CommunicatorBase): self.setup: SetupCommandGroup = SetupCommandGroup(self) # Command groups for stages; Only available for Sentio > 25.2 - self.scope: ScopeCommandGroup = ScopeCommandGroup(self, Stage.Scope, True) + self.scope: StageCommandGroup = StageCommandGroup(self, Stage.Scope, True) + self.chuck: StageCommandGroup = StageCommandGroup(self, Stage.Chuck, False) # deprecated command groups; may be removed at any time. # DO NOT USE THEM IN NEW CODE!