diff --git a/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py index 1b192e8..1f6c512 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,86 @@ 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}.") - def retrieve_substrate_data(self, site: Optional[str] = None) -> Tuple[int, List[str]]: - + # ------------------------------------------------------------------------- + # 1) retrieve_substrate_data + # ------------------------------------------------------------------------- + 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 - - - def get_substrate_type(self, site: Optional[str] = None) -> str: + 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 + # ------------------------------------------------------------------------- + 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 +194,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 +214,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 +222,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 +232,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 +293,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 +301,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 +311,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 +321,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 +329,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 +339,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 +349,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 +357,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 +367,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 +374,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 +382,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 +390,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 +402,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/LoaderCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py index b956cc0..e1cc434 100644 --- a/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py @@ -88,7 +88,6 @@ def prealign(self, marker: OrientationMarker, angle: int) -> None: self.comm.send(f"loader:prealign {marker.toSentioAbbr()}, {angle}") 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. @@ -186,14 +185,12 @@ 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()) return resp.message() - def transfer_wafer( self, src_station: LoaderStation, 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/QAlibriaCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py index e4a6aad..df9b808 100644 --- a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py @@ -1,6 +1,9 @@ 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 sentio_prober_control.Sentio.Enumerations import DriftType +from typing import List class QAlibriaCommandGroup(CommandGroupBase): """ @@ -28,78 +31,49 @@ 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: + def check_calibration_status(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()) + 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(self) -> None: - """ - Deprecated function for verifying calibration drift. - Please use calibration_drift_verify instead. + Raises: + ProberException: If the calibration status is not "OK". """ - self.comm.send("qal:calibration_drift_verify") + self.comm.send("qal:get_calibration_status") Response.check_resp(self.comm.read_line()) - - @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. + def clear_dut_network(self, dut_name: str, drift_type: DriftType, update_ui: bool) -> None: """ - self.comm.send(f"qal:calibration_drift_verify {dut}") - 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() - - # ------------------------------------------------------------------------- - # New / Additional Commands - # ------------------------------------------------------------------------- + Clear network data for a DUT. - def get_calibration_status(self) -> str: - """ - Retrieve the 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: - str: The status string of QAlibria (e.g., "OK"). If "OK" and - the system is in Remote mode, calibration is ready. + None + + Raises: + ProberException: If the remote command returns an error. """ - self.comm.send("qal:get_calibration_status") - resp = Response.check_resp(self.comm.read_line()) - return resp.message() - + 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( 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 @@ -111,14 +85,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()}" @@ -135,18 +113,33 @@ 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()) - def set_ets(self, ports: str, path: str) -> None: + 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. 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()) @@ -164,37 +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: str, 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: The name of the DUT (e.g. "RefDUT"). - drift_type: The type of drift data to clear ("DriftRef" or "Drift"). - update_ui: Whether to update the UI (True/False). + Deprecated function for starting calibration. + Please use calibration_execute instead. """ - cmd = (f"qal:clear_dut_network {dut_name},{drift_type},{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. @@ -212,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. @@ -224,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()) diff --git a/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py new file mode 100644 index 0000000..a9c71a3 --- /dev/null +++ b/sentio_prober_control/Sentio/CommandGroups/SetupCommandGroup.py @@ -0,0 +1,12 @@ +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") + + 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()) 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/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 6d4f701..86515e5 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -1,18 +1,31 @@ from typing import Tuple - -from sentio_prober_control.Sentio.Enumerations import ThermoChuckState +from sentio_prober_control.Sentio.Enumerations import ( + AccessLevel, + ThermoChuckState, + DialogButtons +) from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.ModuleCommandGroupBase import ( ModuleCommandGroupBase, ) - class StatusCommandGroup(ModuleCommandGroupBase): """A command group for getting the status of the probe station and controlling the dashboard module.""" 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. @@ -42,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. @@ -71,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. @@ -108,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. @@ -122,86 +189,92 @@ 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) -> 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. - """ + Args: + enable: A boolean indicating whether to enable (True) or disable (False) high purge + + Returns: + None + """ + self.comm.send(f"status:set_high_purge {enable}") - return Response.check_resp(self.comm.read_line()) \ No newline at end of file + 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. + + 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.toSentioAbbr()},{caption},{level}") + 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. + + 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.toSentioAbbr()},{caption},{level}") + return Response.check_resp(self.comm.read_line()) 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/VisionCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/VisionCommandGroup.py index dd1fdcb..b2ef23a 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. @@ -239,23 +201,20 @@ def switch_all_lights(self, stat: bool) -> Response: 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)) - return Response.check_resp(self.comm.read_line()) - + 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! @@ -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,58 @@ 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. + + 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()) + + def set_lens_zoom_level(self, level: float) -> None: + """Set lens zoom level. + + 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()) + + def get_light_status(self, camera: CameraMountPoint) -> 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.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message().strip().lower() == "1" 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/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/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..805d123 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 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 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: + Row and column index of the home die. + """ + 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: + The number of columns in the wafer map. + """ + 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: + The number of rows in the wafer map. + """ + 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,56 @@ 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[OrientationMarker, float, float]: + """ 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(",") + return OrientationMarker[tok[0]], float(tok[1]), float(tok[2]) + + def get_routing(self) -> Tuple[RoutingStartPoint, RoutingPriority]: + """ 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]), RoutingPriority.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 a16debb..a873a89 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,42 @@ @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. + + Args: + execute: The action to execute. - @deprecated("Use vis.compensation.start_execute(CompensationType.Topography, CompensationMode.Vertical) instead") - def topography(self, execute: ExecuteAction): + 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() + return resp + + 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) -> 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()}") + resp = Response.check_resp(self.comm.read_line()) + + return resp 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/Sentio/CommandGroups/WafermapPathCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapPathCommandGroup.py index d98ed3b..96d2d54 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,17 +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. @@ -39,7 +45,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 +56,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 +68,93 @@ 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 | list[int] | range | list[range]) -> int: + """ + Adds dies with specific bin values to the stepping path. + + This method simplifies usage by accepting native Python types instead of custom strings, + making it easier to dynamically construct input data. + + Args: + 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: + int: The resulting path length after adding the dies. + + Raises: + TypeError: If the input contains unsupported data types. + """ + 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: int | list[int] | range | list[range]) -> int: + """ + Removes dies with specific bin values from the stepping path. + + This method allows more natural Python inputs, avoiding manual string construction. + + Args: + 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: + int: Remaining path length after removal. + + Raises: + TypeError: If the selection type is not supported. + """ + 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/Sentio/CommandGroups/WafermapPoiCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py index f09eaa7..cb4b025 100644 --- a/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/WafermapPoiCommandGroup.py @@ -19,6 +19,21 @@ 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. @@ -32,7 +47,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,8 +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. @@ -57,22 +70,40 @@ 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") + 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. + + 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..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,8 +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 @@ -35,8 +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. @@ -57,7 +56,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,8 +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. @@ -94,11 +91,15 @@ 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()) - def reset(self) -> None: """Reset Sentios subsite definitions. @@ -107,6 +108,23 @@ 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. @@ -123,3 +141,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/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 1910929..b9e791d 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -2,6 +2,32 @@ 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. @@ -42,6 +68,8 @@ class AutoFocusAlgorithm(Enum): Bandpass = 1 Difference = 2 AutoCorrelation = 3 + LaplaceStdDev = 4 + Harris = 5 def toSentioAbbr(self): switcher = { @@ -49,6 +77,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") @@ -141,9 +171,9 @@ 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") @@ -200,6 +230,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): @@ -215,6 +259,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 @@ -223,6 +269,8 @@ class ChuckSite(Enum): AuxRight2 = 3 AuxLeft2 = 4 ChuckCamera = 5 + SiPhSetHoverHeight = 6 + SiPhFiberPowerMeasure = 7 def toSentioAbbr(self): switcher = { @@ -232,9 +280,85 @@ 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) -> "ChuckSite": + 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 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 + 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. @@ -418,6 +542,30 @@ def toSentioAbbr(self): return switcher.get(self, "Invalid CompensationType") +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. @@ -543,6 +691,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") @@ -588,12 +753,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") @@ -608,6 +781,7 @@ class DieNumber(Enum): Present = 1 Selected = 2 + Total = 3 @deprecated("ExecuteAction is deprecated.") @@ -658,7 +832,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. @@ -679,6 +853,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 @@ -815,6 +1000,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 @@ -855,6 +1055,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. @@ -940,6 +1165,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. @@ -1369,6 +1605,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). @@ -1382,7 +1631,7 @@ class RoutingStartPoint(Enum): UpperLeft = 0 UpperRight = 1 - LowerLeft = 2 + LowerLeft = 2 LowerRight = 3 def toSentioAbbr(self): @@ -1394,6 +1643,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. @@ -1436,6 +1698,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. @@ -1458,6 +1750,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. @@ -1480,6 +1784,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. @@ -1594,6 +1920,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. @@ -1625,6 +1977,4 @@ def toSentioAbbr(self): ZPositionHint.Lift: "Lift", ZPositionHint.Transfer: "Transfer", } - return switcher.get(self, "Invalid ZPositionHint") - - + return switcher.get(self, "Invalid ZPositionHint") \ No newline at end of file diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 3c208d0..d865e92 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 @@ -38,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. @@ -95,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: @@ -759,7 +765,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. @@ -823,7 +829,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 @@ -1186,4 +1192,511 @@ 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: + site (ChuckSite): 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()) + site : ChuckSite = ChuckSite.fromSentioAbbr(resp.message()) + + 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: + None + """ + 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: + None + """ + 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: + None + """ + self.comm.send(f"set_chuck_separation_gap {gap}") + Response.check_resp(self.comm.read_line()) + + + 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 + + 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) -> None: + """Move indexer up. + + Returns: + None + """ + self.comm.send("move_indexer_lift") + Response.check_resp(self.comm.read_line()) + + def move_indexer_down(self) -> None: + """Move indexer down. + + Returns: + None + """ + self.comm.send("move_indexer_down") + Response.check_resp(self.comm.read_line()) + + def probe_air_lift(self, valve: str, position: str) -> None: + """Control probe air lift mechanism. + + Args: + valve (str): v1/v2 + position (str): lift or unlift + + Returns: + None + """ + self.comm.send(f"probe_air_lift {valve},{position}") + Response.check_resp(self.comm.read_line()) + + def set_signal_tower(self, red: int, yellow: int, green: int, blue: int) -> None: + """Set signal tower LED states. + + Args: + red (int): -1=no change, 0=off, 1=on, 2=blinking + yellow (int) + green (int) + blue (int) + + Returns: + None + """ + self.comm.send(f"set_signal_tower {red},{yellow},{green},{blue}") + Response.check_resp(self.comm.read_line()) + + 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: None + """ + self.comm.send(f"set_signal_tower_buzzer {state}") + Response.check_resp(self.comm.read_line()) + + def start_move_indexer_pos(self, pos: int) -> None: + """Asynchronously move indexer to position. + + Args: + pos (int): Position index from 1 to 5 + + Returns: + None + """ + self.comm.send(f"start_move_indexer_pos {pos}") + Response.check_resp(self.comm.read_line()) + + def swap_bridge(self, side: str, device_position: str | None = None) -> None: + """Control swap bridge side and position. + + Args: + side (str): right/left/current + device_position (str, optional): up/down + + Returns: + None + """ + cmd = f"swap_bridge {side}" + if device_position: + cmd += f",{device_position}" + self.comm.send(cmd) + 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) -> None: + """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}") + Response.check_resp(self.comm.read_line()) 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/TestLoaderCommandGroup.py b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py index 75beccf..2a13b69 100644 --- a/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py @@ -1,120 +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 -from sentio_prober_control.Sentio.Enumerations import LoaderStation, OrientationMarker, WaferStatusItem - 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_check.return_value.message.return_value = "1" + 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_check.return_value.message.return_value = "OK" - msg = self.loader.load_wafer(LoaderStation.Cassette1, 1, 180) - self.assertEqual(msg, "OK") - 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") - 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" + + 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.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") - - @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") - - @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") - - @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") - - @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") - - @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") - - @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() + 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/TestProberSentio.py b/sentio_prober_control/UnitTest/TestProberSentio.py index 3a81988..e970660 100644 --- a/sentio_prober_control/UnitTest/TestProberSentio.py +++ b/sentio_prober_control/UnitTest/TestProberSentio.py @@ -61,6 +61,99 @@ def test_step_scope_site_next(self): self.assertEqual(offset_x, 1000.0) self.assertEqual(offset_y, 1000.0) + 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_indexer_cda_on(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.test_prober.indexer_cda(True) + self.mock_comm.send.assert_called_with("indexer_cda on") + self.assertIsInstance(resp, Response) + + 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_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_move_indexer_lift(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.test_prober.move_indexer_lift() + self.mock_comm.send.assert_called_with("move_indexer_lift") + self.assertIsInstance(resp, Response) + + def test_move_indexer_down(self): + self.mock_comm.read_line.return_value = "0,0,OK" + resp = self.test_prober.move_indexer_down() + self.mock_comm.send.assert_called_with("move_indexer_down") + self.assertIsInstance(resp, Response) + + def test_probe_air_lift(self): + self.mock_comm.read_line.return_value = "0,0,OK" + 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_signal_tower(self): + self.mock_comm.read_line.return_value = "0,0,OK" + 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_signal_tower_buzzer(self): + self.mock_comm.read_line.return_value = "0,0,OK" + 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_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_swap_bridge(self): + self.mock_comm.read_line.return_value = "0,0,OK" + 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_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_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_door_lock_lock(self): + self.mock_comm.read_line.return_value = "0,0,OK" + 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_door_lock_unlock(self): + self.mock_comm.read_line.return_value = "0,0,OK" + 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__": 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() 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 e25c005..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.message(), "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,9 +96,37 @@ 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): + 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() 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..f98152d --- /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(CameraMountPoint.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() diff --git a/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py new file mode 100644 index 0000000..d6c06fa --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapBinsCommandGroup.py @@ -0,0 +1,87 @@ +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) + 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..58ac073 --- /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, OrientationMarker + + +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, (OrientationMarker.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 new file mode 100644 index 0000000..5aeeeaa --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapCompensationCommandGroup.py @@ -0,0 +1,43 @@ +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" + resp = self.test_prober.map.compensation.topography(ExecuteAction.Execute) + self.mock_comm.send.assert_called_with("map:compensation:topography execute") + self.assertEqual(resp.cmd_id(), 100) + self.assertEqual(resp.message(), "OK") + + 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,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__": + 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() diff --git a/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py new file mode 100644 index 0000000..e1064f0 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapPathCommandGroup.py @@ -0,0 +1,100 @@ +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, PathSelection + + +class TestWafermapPathCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + self.prober = SentioProber(self.mock_comm) + + 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" + 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_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_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_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.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() + + diff --git a/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py b/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py new file mode 100644 index 0000000..fee7022 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapPoiCommandGroup.py @@ -0,0 +1,71 @@ +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_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() + 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,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,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,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,-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" + 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..a5e1754 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestWafermapSubsiteCommandGroup.py @@ -0,0 +1,110 @@ +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_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() + 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()