diff --git a/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py index ab19729..f282456 100644 --- a/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/AuxCommandGroup.py @@ -1,3 +1,4 @@ +from typing import Optional, Tuple, List, Dict, Union from deprecated import deprecated from sentio_prober_control.Sentio.Response import Response @@ -7,11 +8,10 @@ ModuleCommandGroupBase, ) - 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 property - of the SentioProber class. + You are not meant to create instances of this class on your own. Instead, use the aux + property of the SentioProber class. Attributes: cleaning (AuxCleaningGroup): A subgroup to provide logic for probe cleaning. @@ -19,7 +19,369 @@ class AuxCommandGroup(ModuleCommandGroupBase): def __init__(self, comm) -> None: super().__init__(comm, "aux") + self.cleaning: AuxCleaningGroup = AuxCleaningGroup(comm) + + # ------------------------------------------------------------------------- + # 1) retrieve_substrate_data + # ------------------------------------------------------------------------- + def retrieve_substrate_data(self, site: Optional[str] = None) -> Tuple[int, List[str]]: + """ + Retrieves contact and home information from 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"). + 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 + """ + cmd = "aux:retrieve_substrate_data" + if site: + cmd += f" {site}" + + 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 + + # ------------------------------------------------------------------------- + # 2) get_substrate_type + # ------------------------------------------------------------------------- + def get_substrate_type(self, site: Optional[str] = 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"). + If omitted, the currently active site is used. + + Returns: + A string describing the substrate type (e.g. "AC-2", "Wafer", "Brush"). + """ + cmd = "aux:get_substrate_type" + if site: + cmd += f" {site}" + + self.comm.send(cmd) + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + # ------------------------------------------------------------------------- + # 3) step_to_element + # ------------------------------------------------------------------------- + def step_to_element( + self, + element_standard_id: str, + offset_x: float = 0.0, + offset_y: float = 0.0, + motorized_positioner_move: bool = True + ) -> None: + """ + Steps to a calibration element on the currently active chuck site. + + Wraps SENTIO's "aux:step_to_element" remote command. + + Args: + element_standard_id: The standard ID of the element (e.g. "0102"). + offset_x: An X offset (in micrometers). + offset_y: A Y offset (in micrometers). + motorized_positioner_move: If True, move with motorized positioner. + If False, only chuck move. + """ + cmd = ( + f"aux:step_to_element {element_standard_id}," + f"{offset_x},{offset_y},{str(motorized_positioner_move).lower()}" + ) + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + # ------------------------------------------------------------------------- + # 4) step_to_dut_element + # ------------------------------------------------------------------------- + def step_to_dut_element( + self, + dut_name: str, + move_z: bool = True, + x: Optional[float] = None, + y: Optional[float] = None + ) -> None: + """ + Steps to a DUT element on the currently active chuck site. + + Wraps SENTIO's "aux:step_to_dut_element" remote command. + + Args: + dut_name: The name of the DUT element (e.g. "RefDUT"). + move_z: If True, move Z automatically. If False, skip Z movement. + 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}" + + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + # ------------------------------------------------------------------------- + # 5) get_element_type + # ------------------------------------------------------------------------- + def get_element_type( + self, + element_standard_id: str, + site: Optional[str] = None + ) -> str: + """ + Retrieves the type of an element on a calibration substrate placed on the chuck. + + Wraps SENTIO's "aux:get_element_type" remote command. + + 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. + + Returns: + The element type as a string (e.g. "Open", "Short", "Thru", "Align", etc.). + """ + cmd = "aux:get_element_type" + if site: + cmd += f" {site},{element_standard_id}" + else: + cmd += f" {element_standard_id}" + + self.comm.send(cmd) + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + # ------------------------------------------------------------------------- + # 6) get_substrate_info + # ------------------------------------------------------------------------- + def get_substrate_info(self, site: Union[int, str]) -> Dict[str, Union[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. + + Returns: + A dict with keys: + "substrate_type" (str), + "substrate_id" (str), + "life_time" (float) in % + """ + cmd = f"aux:get_substrate_info {site}" + 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]) + } + + # ------------------------------------------------------------------------- + # 7) get_element_touch_count + # ------------------------------------------------------------------------- + def get_element_touch_count( + self, + element_standard_id: str, + site: Optional[str] = None + ) -> int: + """ + Retrieves the touch count of an element on a calibration substrate placed on the chuck. + + Wraps SENTIO's "aux:get_element_touch_count" remote command. + + 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. + + Returns: + The touch count (int) of the element. + """ + cmd = "aux:get_element_touch_count" + if site: + cmd += f" {site},{element_standard_id}" + else: + cmd += f" {element_standard_id}" + + self.comm.send(cmd) + resp = Response.check_resp(self.comm.read_line()) + return int(resp.message()) + + # ------------------------------------------------------------------------- + # 8) get_element_spacing + # ------------------------------------------------------------------------- + def get_element_spacing( + self, + element_standard_id: str, + site: Optional[str] = None + ) -> float: + """ + Retrieves the spacing value of an element on a calibration substrate placed on the chuck. + + Wraps SENTIO's "aux:get_element_spacing" remote command. + + 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. + + Returns: + The spacing in micrometers (float). + """ + cmd = "aux:get_element_spacing" + if site: + cmd += f" {site},{element_standard_id}" + else: + cmd += f" {element_standard_id}" + + self.comm.send(cmd) + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + # ------------------------------------------------------------------------- + # 9) get_element_pos + # ------------------------------------------------------------------------- + def get_element_pos( + self, + element_standard_id: str, + site: Optional[str] = None + ) -> Tuple[float, float]: + """ + Retrieves the (X, Y) position of an element on a calibration substrate placed on the chuck. + + Wraps SENTIO's "aux:get_element_pos" remote command. + + 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. + + Returns: + A tuple (x_position, y_position) in micrometers (floats). + """ + cmd = "aux:get_element_pos" + if site: + cmd += f" {site},{element_standard_id}" + else: + cmd += f" {element_standard_id}" + + self.comm.send(cmd) + resp = Response.check_resp(self.comm.read_line()) + 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) + + # ------------------------------------------------------------------------- + # 10) get_element_life_time + # ------------------------------------------------------------------------- + def get_element_life_time( + self, + element_standard_id: str, + site: Optional[str] = None + ) -> float: + """ + Retrieves the life time (in %) of an element on a calibration substrate placed on the chuck. + + Wraps SENTIO's "aux:get_element_life_time" remote command. + + 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. + + Returns: + The life time percentage (float). + """ + cmd = "aux:get_element_life_time" + if site: + cmd += f" {site},{element_standard_id}" + else: + cmd += f" {element_standard_id}" + + self.comm.send(cmd) + resp = Response.check_resp(self.comm.read_line()) + return float(resp.message()) + + # ------------------------------------------------------------------------- + # 11) get_element_info + # ------------------------------------------------------------------------- + def get_element_info( + self, + element_standard_id: str, + site: Optional[str] = None + ) -> Dict[str, Union[str, float, int]]: + """ + Retrieves information of a calibration element on a calibration substrate placed on the chuck. + + Wraps SENTIO's "aux:get_element_info" remote command. + + 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. + + 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 % + """ + cmd = "aux:get_element_info" + if site: + cmd += f" {site},{element_standard_id}" + else: + cmd += f" {element_standard_id}" - self.cleaning : AuxCleaningGroup = AuxCleaningGroup(comm) + 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 diff --git a/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py index 55ad43b..f7383bb 100644 --- a/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/LoaderCommandGroup.py @@ -60,8 +60,8 @@ def load_wafer(self, src_station: LoaderStation, src_slot: int, angle: int | Non else: self.comm.send(f"loader:load_wafer {src_station.toSentioAbbr()}, {src_slot}, {angle}") - Response.check_resp(self.comm.read_line()) - + resp = Response.check_resp(self.comm.read_line()) + return resp.message() def prealign(self, marker: OrientationMarker, angle: int) -> None: """Prealign a wafer. @@ -74,8 +74,8 @@ def prealign(self, marker: OrientationMarker, angle: int) -> None: """ self.comm.send(f"loader:prealign {marker.toSentioAbbr()}, {angle}") - Response.check_resp(self.comm.read_line()) - + resp = Response.check_resp(self.comm.read_line()) + return resp.message() def query_wafer_status(self, station : LoaderStation, slot : int) -> Tuple[LoaderStation, int, int, int, float] | None: """Query the status of a wafer in a loader station. @@ -143,8 +143,8 @@ def set_wafer_status(self, station: LoaderStation, slot : int, what : WaferStatu value (float): The value to set. """ self.comm.send(f"loader:set_wafer_status {station.toSentioAbbr()},{slot},{what.toSentioAbbr()},{value}") - Response.check_resp(self.comm.read_line()) - + resp = Response.check_resp(self.comm.read_line()) + return resp.message() def start_prepare_station(self, station: LoaderStation, angle: float | None = None) -> None: """Prepare a loader station for wafer processing. @@ -164,8 +164,8 @@ def start_prepare_station(self, station: LoaderStation, angle: float | None = No else: self.comm.send(f"loader:start_prepare_station {station.toSentioAbbr()}, {angle}") - Response.check_resp(self.comm.read_line()) - + resp = Response.check_resp(self.comm.read_line()) + return resp.message() @deprecated("duplicate functionality; Use SentioProber.move_chuck_work_area!") def switch_work_area(self, area: str): @@ -192,7 +192,8 @@ def transfer_wafer( self.comm.send( f"loader:transfer_wafer {src_station.toSentioAbbr()}, {src_slot}, {dst_station.toSentioAbbr()}, {dst_slot}" ) - Response.check_resp(self.comm.read_line()) + resp = Response.check_resp(self.comm.read_line()) + return resp.message() def unload_wafer(self) -> None: @@ -202,4 +203,103 @@ def unload_wafer(self) -> None: """ self.comm.send("loader:unload_wafer") - Response.check_resp(self.comm.read_line()) + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def has_cassette(self,station : LoaderStation) -> None: + + """Query whether a cassette is present in a given cassette station. + + Args: + station (LoaderStation): The cassette station to scan + + """ + + self.comm.send(f"loader:has_cassette {station.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def set_wafer_id(self,station : LoaderStation, slot : int, waferid : str) -> None: + + """Reset the wafer id. + + Args: + station (LoaderStation): The station to reset waferid + slot(int):The slot to reset waferid + + """ + + self.comm.send(f"loader:set_wafer_id {station.toSentioAbbr}, {slot}, {waferid}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def query_wafer_id(self,station : LoaderStation, slot : int) -> None: + + """Query the wafer id that already existing. + + Args: + station (LoaderStation): The station to query waferid + slot(int):The slot to query waferid + """ + + self.comm.send(f"loader:query_wafer_id {station.toSentioAbbr}, {slot}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def read_wafer_id(self,angle : str, side : str) -> None: + + """Command will trigger ID Reader to read wafer id. + + Args: + angle(str):Wafer ID angle + side(str):Wafer ID side + """ + + self.comm.send(f"loader:read_wafer_id {angle}, {side}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def start_prepare_wafer(self,station : LoaderStation, slot : int, angle : int, readid : int, unloadstation : LoaderStation, unloadslot : int) -> None: + + """Set the wafer to default state + + """ + + self.comm.send(f"loader:start_prepare_wafer {station.toSentioAbbr}, {slot}, {angle}, {readid}, {unloadstation}, {unloadslot}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def swap_wafer(self) ->None: + + """ + For dual-fork loader: Wafer form chuck is placed on loader fork A, wafer from loader fork A is placed on chuck. + For single-fork loader: Wafer form chuck is placed on its origin station, wafer from pre-aligner is placed on chuck. + + """ + + self.comm.send(f"loader:swap_wafer") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def query_station_status(self, station : LoaderStation) ->None: + + """Returns the wafer presence state of a station without losing wafer information. + + Args: + station(LoaderStation):query station status. + + """ + + self.comm.send(f"loader:query_station_status {station.toSentioAbbr}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def start_read_wafer_id(self, angle : str, side : str) ->None: + + """Command will trigger ID Reader to read wafer id._summary_ + + """ + + self.comm.send(f"loader:start_read_wafer_id {angle}, {side}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() \ No newline at end of file diff --git a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py index d2e3296..dd81592 100644 --- a/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/ProbeCommandGroup.py @@ -83,7 +83,7 @@ def async_step_probe_site_first(self, probe: ProbeSentio) -> int: return resp.cmd_id() - def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[int, str, float, float]: + def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[int, float, float, str]: """Get information for a probe site. Each positioner can define n a number of predefined positions called "sites". @@ -93,13 +93,13 @@ def get_probe_site(self, probe: ProbeSentio, idx: int) -> Tuple[int, str, float, probe: The probe to get the site for. Returns: - A tuple containing the site index, the site name, the x position in micrometer and the y position in micrometer. + A tuple containing the site index, position x, the position y in micrometer and the reference. """ self.comm.send(f"get_positioner_site {probe.toSentioAbbr()},{idx}") resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") - return int(tok[0]), str(tok[1]), float(tok[2]), float(tok[3]) + return str(tok[0]), float(tok[1]), float(tok[2]), str(tok[3]) def get_probe_site_number(self, probe: ProbeSentio) -> int: @@ -144,7 +144,7 @@ def get_probe_z(self, probe: ProbeSentio, ref: ProbeZReference) -> float: Returns: The z position in micrometer. """ - self.comm.send(f"get_positioner_z {probe.toSentioAbbr()}, {ref.toSentioAbbr()}") + self.comm.send(f"get_positioner_z {probe.toSentioAbbr()},{ref.toSentioAbbr()}") resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) @@ -226,7 +226,7 @@ def move_probe_z(self, probe: ProbeSentio, ref: ProbeZReference, z: float) -> fl The z position after the move in micrometer (from zero). """ - self.comm.send(f"move_positioner_z {probe.toSentioAbbr()}, {ref.toSentioAbbr()}, {z}") + self.comm.send(f"move_positioner_z {probe.toSentioAbbr()},{ref.toSentioAbbr()},{z}") resp = Response.check_resp(self.comm.read_line()) return float(resp.message()) @@ -259,7 +259,7 @@ def set_probe_home(self, probe: ProbeSentio, site: ChuckSite | None = None, x: f if site is None: self.comm.send(f"set_positioner_home {probe.toSentioAbbr()}") else: - self.comm.send(f"set_positioner_home {probe.toSentioAbbr()}, {site.toSentioAbbr()}, {x}, {y}") + self.comm.send(f"set_positioner_home {probe.toSentioAbbr()},{site.toSentioAbbr()},{x},{y}") Response.check_resp(self.comm.read_line()) @@ -321,3 +321,47 @@ def step_probe_site_next(self, probe: ProbeSentio) -> Tuple[str, float, float]: resp = Response.check_resp(self.comm.read_line()) tok = resp.message().split(",") return tok[0], float(tok[1]), float(tok[2]) + + def enable_probe_motor(self, probe: ProbeSentio, status: bool) -> None: + """Enable/Disable the probe motor. + + Probe with 3 motors will enable and disable by following behavior. + + Args: + probe: The probe to action. + status: Enable or disable status + + """ + self.comm.send(f"enable_positioner_motor {probe.toSentioAbbr()},{status}") + Response.check_resp(self.comm.read_line()) + + def get_probe_status(self, probe: ProbeSentio) -> str: + """Obtain the status of probe. + + Command will return 4 probe status + + Args: + probe: The probe to step. + + Returns: + Status of positioner with 4 digits, 1st digit indicates the East Positioner, 2nd digit indicates West Positioner + 3rd digit indicates the North Positioner, 4rd digit indicates the South Positioner. + """ + + self.comm.send(f"get_positioner_status {probe.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def set_probe_status(self, probe: ProbeSentio, status: bool) -> None: + """Enable/Disable the probe stage in the SENTIO. + + Enable/Disable the Probes in the SENTIO. + + Args: + probe: The probe to action. + status: Enable or disable status + + """ + self.comm.send(f"set_positioner_status {probe.toSentioAbbr()},{status}") + Response.check_resp(self.comm.read_line()) + diff --git a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py index 675641c..4a98acb 100644 --- a/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/QAlibriaCommandGroup.py @@ -1,66 +1,217 @@ from deprecated import deprecated - from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase - class QAlibriaCommandGroup(CommandGroupBase): - """This command group contains functions for working with Qalibria.""" + """ + This command group contains functions for working with QAlibria. + """ def __init__(self, comm) -> None: super().__init__(comm) - def calibration_execute(self) -> None: - """Execute VNA calibration. + """ + Execute VNA calibration. Wraps SENTIO's "qal:calibration_execute" remote command. """ self.comm.send("qal:calibration_execute") Response.check_resp(self.comm.read_line()) - def calibration_drift_verify(self, dut_name: str = "", auto_exec: bool = True) -> None: - """Calibration Drift verification. + """ + Calibration Drift verification. Wraps SENTIO's "qal:calibration_drift_verify" remote command. Args: - dut_name: The name of the dut. - auto_exec: + dut_name: The name of the DUT. + auto_exec: Whether to automatically execute the drift verify process. """ - self.comm.send("qal:calibration_drift_verify {dut_name}, {auto_exec}") - resp = Response.check_resp(self.comm.read_line()) + self.comm.send(f"qal:calibration_drift_verify {dut_name},{str(auto_exec).lower()}") + Response.check_resp(self.comm.read_line()) - # Inconsistent API filed as CR#13887. This function should not exist! - # the use of the "start_" prefix implies it is an async function. It is not or - # if it is where it the command id return value? - @deprecated(reason="use calibration_execute instead!; this function violates naming conventions since its name is different from the remote command; filed as CR#13887") + @deprecated(reason="use calibration_execute instead!; violates naming conventions. (CR#13887)") def start_calibration(self) -> None: + """ + Deprecated function for starting calibration. + Please use calibration_execute instead. + """ self.comm.send("qal:calibration_execute") Response.check_resp(self.comm.read_line()) - - # Inconsistent API filed as CR#13887. This function should not exist! - @deprecated(reason="use calibration_drift_verify instead!; this function violates naming conventions since its name is different from the remote command; filed as CR#13887") + @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. + """ self.comm.send("qal:calibration_drift_verify") Response.check_resp(self.comm.read_line()) - - # Inconsistent API filed as CR#13887. This function should not exist! - @deprecated(reason="use calibration_drift_verify instead!; this function violates naming conventions since its name is different from the remote command; filed as CR#13887") + @deprecated(reason="use calibration_drift_verify instead!; violates naming conventions. (CR#13887)") def verify_calibration_drift_dut(self, dut) -> None: - self.comm.send("qal:calibration_drift_verify {}".format(dut)) + """ + 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()) - - # Inconsistent API filed as CR#13887. This function should not exist! @deprecated(reason="oddly specific function name; filed as CR#13887") def set_calibration_drift_probe12(self): - self.comm.send(" qal:set_dut_network RefDUT,DriftRef,12,false") + """ + 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 + # ------------------------------------------------------------------------- + + def get_calibration_status(self) -> str: + """ + Retrieve the status of QAlibria. + + Wraps SENTIO's "qal:get_calibration_status" remote command. + + Returns: + str: The status string of QAlibria (e.g., "OK"). If "OK" and + the system is in Remote mode, calibration is ready. + """ + self.comm.send("qal:get_calibration_status") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() + + def measurement_execute( + self, + file_name: str, + ports: str = "1,2", + correct_by_vna: bool = True, + enable_use_ratio: bool = False, + enable_switch_term: bool = False + ) -> None: + """ + Execute a VNA measurement. + + Wraps SENTIO's "qal:measurement_execute" remote command. + + Args: + file_name: The path to the SNP file. + ports: The port(s) 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). + """ + cmd = ( + f"qal:measurement_execute " + f"{file_name},{ports}," + f"{str(correct_by_vna).lower()}," + f"{str(enable_use_ratio).lower()}," + f"{str(enable_switch_term).lower()}" + ) + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + def reset_ets(self) -> None: + """ + Reset error terms in the buffer. + + Wraps SENTIO's "qal:reset_ets" remote command. + """ + self.comm.send("qal:reset_ets") + Response.check_resp(self.comm.read_line()) + + def set_ets(self, ports: str, path: str) -> 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"). + path: Path to the error terms file in the buffer (e.g. "D:\\temp\\ets.txt"). + """ + cmd = f"qal:set_ets {ports},{path}" + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + def send_ets_to_vna(self, cal_set_name: str) -> None: + """ + Send current error terms in QAlibria buffer to the VNA. + + Wraps SENTIO's "qal:send_ets_to_vna" remote command. - self.comm.send(" qal:set_dut_network RefDUT,Drift,12,false") + Args: + cal_set_name: The name of the cal set in the VNA (e.g. "cal_set_p12"). + """ + cmd = f"qal:send_ets_to_vna {cal_set_name}" + 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: + """ + 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). + """ + cmd = ( + f"qal:clear_dut_network " + f"{dut_name},{drift_type},{str(update_ui).lower()}" + ) + self.comm.send(cmd) + Response.check_resp(self.comm.read_line()) + + def vna_write(self, vna_command: str) -> None: + """ + Write a remote command to the VNA. + + 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()) + + def vna_query(self, vna_command: str) -> str: + """ + Query a remote command from the VNA and return the response. + + Wraps SENTIO's "qal:vna_query" remote command. + + Args: + vna_command: The remote command to query the VNA (e.g. ":SENS1:FREQ:STAR?"). + + Returns: + str: The response string from the VNA. + """ + cmd = f"qal:vna_query {vna_command}" + self.comm.send(cmd) resp = Response.check_resp(self.comm.read_line()) return resp.message() + + def vna_read(self) -> str: + """ + Read data from the VNA. + + Wraps SENTIO's "qal:vna_read" remote command. + + Returns: + str: The data in the VNA buffer. + """ + self.comm.send("qal:vna_read") + resp = Response.check_resp(self.comm.read_line()) + return resp.message() \ No newline at end of file diff --git a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py index 464e969..2df8301 100644 --- a/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/SiPHCommandGroup.py @@ -1,6 +1,6 @@ from typing import Tuple -from sentio_prober_control.Sentio.Enumerations import ProbeSentio +from sentio_prober_control.Sentio.Enumerations import ProbeSentio, UvwAxis, FiberType from sentio_prober_control.Sentio.Response import Response from sentio_prober_control.Sentio.CommandGroups.CommandGroupBase import CommandGroupBase @@ -15,11 +15,11 @@ def __init__(self, comm) -> None: super().__init__(comm) - def fast_alignment(self) -> Response: + def fast_alignment(self) -> None: """Perform fast fiber alignment.""" self.comm.send("siph:fast_alignment") - return Response.check_resp(self.comm.read_line()) + Response.check_resp(self.comm.read_line()) def get_cap_sensor(self) -> Tuple[float, float]: @@ -77,3 +77,220 @@ def move_separation(self, probe: ProbeSentio) -> None: self.comm.send(f"siph:move_separation {probe.toSentioAbbr()}") Response.check_resp(self.comm.read_line()) + + def coupling(self, probe: ProbeSentio, axis: UvwAxis) -> None: + """Start execute coupling . + + Args: + probe: Execute probe. + axis: Execute axis + """ + + self.comm.send(f"siph:coupling {probe.toSentioAbbr()},{axis.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def get_alignment(self, probe: ProbeSentio, fiber_type: FiberType) -> Tuple[bool, bool, bool, bool]: + """Get the fast alignment function enable including Coarse, Fine, Gradient, and Rotary/Focal searching. + + Args: + probe: The probe to get the alignment settings for. + fiber_type: The type of fiber used (Single, Array, or Lensed). + + Returns: + A tuple containing the status of Coarse, Fine, Gradient, and Rotary/Focal searching (True/False). + """ + self.comm.send(f"siph:get_alignment {probe.toSentioAbbr()},{fiber_type.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + + tok = resp.message().split(",") + coarse = tok[0].strip().lower() == "true" + fine = tok[1].strip().lower() == "true" + gradient = tok[2].strip().lower() == "true" + rotary_focal = tok[3].strip().lower() == "true" + + return coarse, fine, gradient, rotary_focal + + def set_origin(self, probe: ProbeSentio) -> None: + """Set the current position as the origin position for the SiPH positioner. + + Args: + probe: The probe to set the origin position for (East or West). + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:set_origin {probe.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def move_origin(self, probe: ProbeSentio) -> None: + """Move SiPH positioner to its origin position. + + The movement includes: + - NanoCube XY moves back to 50 μm. + - UVW axes move back to the position set during Hover Height training. + - If the axis is in "Manual" mode, only NanoCube moves to 50 μm. + + Args: + probe: The probe to move to origin position (East or West). + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:move_origin {probe.toSentioAbbr()}") + return Response.check_resp(self.comm.read_line()) + + def move_position_uvw(self, probe: ProbeSentio, axis: UvwAxis, degree: float) -> float: + """Move the SiPH positioner target axis with a relative degree. + + Args: + probe: The positioner ID to move (East or West). + axis: The axis to move (U, V, or W). + degree: The relative degree to move. + + Returns: + The current position of the axis after movement. + """ + self.comm.send(f"siph:move_position_uvw {probe.toSentioAbbr()},{axis.toSentioAbbr()},{degree}") + resp = Response.check_resp(self.comm.read_line()) + + return float(resp.message()) + + def pivot_point(self, probe: ProbeSentio) -> None: + """Run pivot point calibration for the specified positioner. + + Args: + probe: The positioner ID to calibrate (East or West). + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:pivot_point {probe.toSentioAbbr()}") + Response.check_resp(self.comm.read_line()) + + def set_alignment(self, probe: ProbeSentio, fiber_type: FiberType, coarse: bool, fine: bool, gradient: bool, + rotary: bool) -> None: + """Set the fast alignment function enable including Coarse, Fine, Gradient, and Rotary/Focal searching. + + Args: + probe: The positioner ID to calibrate (East or West). + fiber_type: The fiber type ("Single", "Array", "Lensed"). + coarse: Enable or disable coarse search (True = ON, False = OFF). + fine: Enable or disable fine search (True = ON, False = OFF). + gradient: Enable or disable gradient search (True = ON, False = OFF). + rotary: Enable or disable rotary/focal search (True = ON, False = OFF, not supported for "Single" fiber type). + + Returns: + A Response object containing the command execution status. + """ + coarse_str = "ON" if coarse else "OFF" + fine_str = "ON" if fine else "OFF" + gradient_str = "ON" if gradient else "OFF" + rotary_str = "ON" if rotary else "OFF" + + self.comm.send( + f"siph:set_alignment {probe.toSentioAbbr()},{fiber_type},{coarse_str},{fine_str},{gradient_str},{rotary_str}") + Response.check_resp(self.comm.read_line()) + + def set_pivot_point(self, rotary_angle_1: float, rotary_angle_2: float, leveling_angle: float, repeats: int) -> None: + """Set the parameters for the pivot point function. + + Args: + rotary_angle_1: The first rotary angle. + rotary_angle_2: The second rotary angle. + leveling_angle: The leveling angle. + repeats: The number of repetitions. + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:set_pivot_point {rotary_angle_1},{rotary_angle_2},{leveling_angle},{repeats}") + Response.check_resp(self.comm.read_line()) + + def download_graph_data(self, file_path: str, file_name: str) -> None: + """Download the graph data and save it to the specified location. + + Args: + file_path: The directory path where the graph data will be saved. + file_name: The name of the file to save the graph data. + + Returns: + A Response object containing the command execution status. + """ + self.comm.send(f"siph:download_graph_data {file_path}, {file_name}") + Response.check_resp(self.comm.read_line()) + + def start_tracking(self, timeout: int = 60) -> int: + """Start the SiPH positioner gradient tracking search asynchronously. + + Args: + timeout: Timeout value in seconds (range: 1~600). Default is 60 sec. + + Returns: + The asynchronous command ID, which can be used to check status or abort. + """ + self.comm.send(f"siph:start_tracking {timeout}") + resp = Response.check_resp(self.comm.read_line()) + + # Extract asynchronous command ID from response + command_id = int(resp.cmd_id()) + return command_id + + def move_nanocube_xy(self, probe: ProbeSentio, x: float, y: float) -> tuple[float, float]: + """Move NanoCube to the target XY position. + + The movement range is limited to 0 ~ 100 μm. + + Args: + probe: The positioner (East or West). + x: Target X position (μm), must be in range [0, 100]. + y: Target Y position (μm), must be in range [0, 100]. + + Returns: + A tuple containing the new X and Y positions after movement. + """ + if not (0 <= x <= 100 and 0 <= y <= 100): + raise ValueError("X and Y values must be between 0 and 100 μm.") + + self.comm.send(f"siph:move_nanocube_xy {probe.toSentioAbbr()},{x},{y}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + new_x = float(tok[0]) + new_y = float(tok[1]) + return new_x, new_y + + def get_nanocube_xy(self, probe: ProbeSentio) -> tuple[float, float]: + """Get the current NanoCube XY position. + + Args: + probe: The positioner (East or West). + + Returns: + A tuple containing the current X and Y positions. + """ + self.comm.send(f"siph:get_nanocube_xy {probe.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + current_x = float(tok[0]) + current_y = float(tok[1]) + return current_x, current_y + + def get_nanocube_z(self, probe: ProbeSentio) -> float: + """Get the current NanoCube Z position. + + Args: + probe: The positioner (East or West). + + Returns: + The current Z position. + """ + self.comm.send(f"siph:get_nanocube_z {probe.toSentioAbbr()}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + current_z = float(tok[0]) + return current_z \ No newline at end of file diff --git a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py index 7f0e247..6d4f701 100644 --- a/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py +++ b/sentio_prober_control/Sentio/CommandGroups/StatusCommandGroup.py @@ -120,7 +120,88 @@ def set_chuck_temp(self, temp: float, lift_chuck : bool = False) -> None: Raises: ProberException: If an error occurred. """ - 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: + """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. + + 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()) + + def set_chuck_thermo_hold_mode(self, mode: bool) -> Response: + """Set thermo chuck hold mode. + + Args: + mode: A boolean indicating whether to enable (True) or disable (False) hold mode. + + Returns: + A Response object confirming the command execution. + """ + self.comm.send(f"status:set_chuck_thermo_hold_mode {mode}") + return Response.check_resp(self.comm.read_line()) + + def set_chuck_thermo_mode(self, mode: str) -> Response: + """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. + + 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()) + + def set_high_purge(self, enable: bool) -> Response: + """Set thermo chuck high purge state. + + Args: + enable: A boolean indicating whether to enable (True) or disable (False) high purge. + Returns: + A Response object confirming the command execution. + """ + self.comm.send(f"status:set_high_purge {enable}") + return Response.check_resp(self.comm.read_line()) \ No newline at end of file diff --git a/sentio_prober_control/Sentio/Enumerations.py b/sentio_prober_control/Sentio/Enumerations.py index 97e56d9..1974945 100644 --- a/sentio_prober_control/Sentio/Enumerations.py +++ b/sentio_prober_control/Sentio/Enumerations.py @@ -1550,3 +1550,45 @@ def toSentioAbbr(self): ZPositionHint.Transfer: "Transfer", } return switcher.get(self, "Invalid ZPositionHint") + +class UvwAxis(Enum): + """An enumeration containing UVW axis. + + Attributes: + U (0): U axis. + V (1): V axis. + W (2): W axis. + """ + + U = 0 + V = 1 + W = 2 + + def toSentioAbbr(self): + switcher = { + UvwAxis.U: "U", + UvwAxis.V: "V", + UvwAxis.W: "W", + } + return switcher.get(self, "Invalid UVW enumerator") + +class FiberType(Enum): + """An enumeration containing supported fiber type. + + Attributes: + Single (0) + Array (1) + Lensed (2) + """ + + Single = 0 + Array = 1 + Lensed = 2 + + def toSentioAbbr(self): + switcher = { + FiberType.Single: "Single", + FiberType.Array: "Array", + FiberType.Lensed: "Lensed", + } + return switcher.get(self, "Invalid fiber type enumerator") diff --git a/sentio_prober_control/Sentio/ProberSentio.py b/sentio_prober_control/Sentio/ProberSentio.py index 7e3101d..4d25601 100644 --- a/sentio_prober_control/Sentio/ProberSentio.py +++ b/sentio_prober_control/Sentio/ProberSentio.py @@ -1050,3 +1050,98 @@ def wait_complete(self, id_or_resp: int | Response, timeout: int = 300) -> Respo self.comm.send(f"wait_complete {id_or_resp}, {timeout}") return Response.check_resp(self.comm.read_line()) + + def get_scope_home(self) -> tuple[float, float]: + """Gets the home position information for the scope stage. + + Returns: + A tuple containing the X and Y home positions in micrometers. + """ + self.comm.send("get_scope_home") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + home_x = float(tok[0]) + home_y = float(tok[1]) + return home_x, home_y + + def set_scope_home(self, home_x: float = None, home_y: float = None) -> None: + """Sets the home position for the scope stage. + + Args: + home_x: (Optional) The X-coordinate of the home position in micrometers. + home_y: (Optional) The Y-coordinate of the home position in micrometers. + + Returns: + A Response object indicating whether the command was successful. + """ + if home_x is not None and home_y is not None: + self.comm.send(f"set_scope_home {home_x},{home_y}") + else: + self.comm.send("set_scope_home") + + Response.check_resp(self.comm.read_line()) + + def step_scope_site(self, site_index: int or str) -> tuple[str, float, float]: + """Steps the scope to the indicated site and sets it as the current site. + + Args: + site_index: The scope site index (zero-based) or its ID. + + Returns: + A tuple containing: + - Site ID (string): The identifier of the current scope site. + - Offset X (float): The X offset relative to the scope home position. + - Offset Y (float): The Y offset relative to the scope home position. + """ + self.comm.send(f"step_scope_site {site_index}") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_id = tok[0] + offset_x = float(tok[1]) + offset_y = float(tok[2]) + + return site_id, offset_x, offset_y + + def step_scope_site_first(self) -> tuple[str, float, float]: + """Steps the scope to the first site in the scope site list. + + Returns: + A tuple containing: + - Site ID (string): The identifier of the current scope site. + - Offset X (float): The X offset relative to the scope home position. + - Offset Y (float): The Y offset relative to the scope home position. + """ + self.comm.send("step_scope_site_first") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_id = tok[0] + offset_x = float(tok[1]) + offset_y = float(tok[2]) + + return site_id, offset_x, offset_y + + def step_scope_site_next(self) -> tuple[str, float, float]: + """Steps the scope to the next site and sets it as the current site. + + Returns: + A tuple containing: + - Site ID (string): The identifier of the current scope site. + - Offset X (float): The X offset relative to the scope home position. + - Offset Y (float): The Y offset relative to the scope home position. + """ + self.comm.send("step_scope_site_next") + resp = Response.check_resp(self.comm.read_line()) + + # Parse response message + tok = resp.message().split(",") + site_id = tok[0] + offset_x = float(tok[1]) + offset_y = float(tok[2]) + + return site_id, offset_x, offset_y \ No newline at end of file diff --git a/sentio_prober_control/UnitTest/TestAuxCommandGroup.py b/sentio_prober_control/UnitTest/TestAuxCommandGroup.py new file mode 100644 index 0000000..dd7e040 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestAuxCommandGroup.py @@ -0,0 +1,200 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Sentio.Response import Response + +# A FakeResponse class that strips the "0,0," prefix from the response. +class FakeResponse: + def __init__(self, message): + self._message = message + + def message(self): + prefix = "0,0," + if self._message.startswith(prefix): + return self._message[len(prefix):] + return self._message + +# Save the original Response.check_resp so we can restore it after tests. +_original_check_resp = Response.check_resp +# Patch Response.check_resp to wrap the raw line in a FakeResponse. +Response.check_resp = lambda line: FakeResponse(line) + +class TestAuxCommandGroup(unittest.TestCase): + def setUp(self): + # Create a mock communicator based on CommunicatorTcpIp. + 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 + + def tearDown(self): + # Restore the original Response.check_resp. + Response.check_resp = _original_check_resp + + # 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() + self.mock_comm.send.assert_called_with("aux:retrieve_substrate_data") + self.assertEqual(count, 3) + self.assertEqual(sites, ["Aux1", "Aux2", "Aux3"]) + + 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"]) + + # 2) Test get_substrate_type + def test_get_substrate_type_no_site(self): + self.mock_comm.read_line.return_value = "0,0,Wafer" + result = self.aux.get_substrate_type() + self.mock_comm.send.assert_called_with("aux:get_substrate_type") + self.assertEqual(result, "Wafer") + + 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") + self.assertEqual(result, "AC-2") + + # 3) Test step_to_element + def test_step_to_element(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.aux.step_to_element("0102", offset_x=100.0, offset_y=200.0, motorized_positioner_move=True) + expected_command = "aux:step_to_element 0102,100.0,200.0,true" + self.mock_comm.send.assert_called_with(expected_command) + + # 4) Test step_to_dut_element (without and with XY coordinates) + def test_step_to_dut_element_without_xy(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.aux.step_to_dut_element("RefDUT", move_z=False) + expected_command = "aux:step_to_dut_element RefDUT,false" + self.mock_comm.send.assert_called_with(expected_command) + + def test_step_to_dut_element_with_xy(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.aux.step_to_dut_element("RefDUT", move_z=True, x=150100, y=149000) + expected_command = "aux:step_to_dut_element RefDUT,true,150100,149000" + self.mock_comm.send.assert_called_with(expected_command) + + # 5) Test get_element_type + def test_get_element_type_without_site(self): + self.mock_comm.read_line.return_value = "0,0,Short" + 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") + + 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" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, "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" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, {"substrate_type": "AC-2", "substrate_id": "ac2", "life_time": 4.48}) + + # 7) Test get_element_touch_count + def test_get_element_touch_count_without_site(self): + self.mock_comm.read_line.return_value = "0,0,5" + result = self.aux.get_element_touch_count("0102") + expected_command = "aux:get_element_touch_count 0102" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, 5) + + 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" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, 7) + + # 8) Test get_element_spacing + def test_get_element_spacing_without_site(self): + self.mock_comm.read_line.return_value = "0,0,150.0" + result = self.aux.get_element_spacing("0102") + expected_command = "aux:get_element_spacing 0102" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, 150.0) + + 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" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, 200.5) + + # 9) Test get_element_pos + def test_get_element_pos_without_site(self): + self.mock_comm.read_line.return_value = "0,0,150.0,250.0" + result = self.aux.get_element_pos("0102") + expected_command = "aux:get_element_pos 0102" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, (150.0, 250.0)) + + 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" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, (100.0, 200.0)) + + # 10) Test get_element_life_time + def test_get_element_life_time_without_site(self): + self.mock_comm.read_line.return_value = "0,0,90.0" + result = self.aux.get_element_life_time("0102") + expected_command = "aux:get_element_life_time 0102" + self.mock_comm.send.assert_called_with(expected_command) + self.assertEqual(result, 90.0) + + 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" + 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): + 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) + + 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) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py new file mode 100644 index 0000000..75beccf --- /dev/null +++ b/sentio_prober_control/UnitTest/TestLoaderCommandGroup.py @@ -0,0 +1,120 @@ +import unittest +from unittest.mock import MagicMock, patch +from sentio_prober_control.Sentio.CommandGroups.LoaderCommandGroup import LoaderCommandGroup +from sentio_prober_control.Sentio.Enumerations import LoaderStation, OrientationMarker, WaferStatusItem + + +class TestLoaderCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_parent = MagicMock() + self.mock_comm = 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" + 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" + 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() diff --git a/sentio_prober_control/UnitTest/TestProbeCommandGroup.py b/sentio_prober_control/UnitTest/TestProbeCommandGroup.py index 8106552..1325e6e 100644 --- a/sentio_prober_control/UnitTest/TestProbeCommandGroup.py +++ b/sentio_prober_control/UnitTest/TestProbeCommandGroup.py @@ -1,9 +1,6 @@ import unittest from unittest.mock import MagicMock -from sentio_prober_control.Communication.CommunicatorBase import CommunicatorBase from sentio_prober_control.Sentio.Enumerations import ProbeSentio, ProbeXYReference, ProbeZReference -from sentio_prober_control.Sentio.Response import Response -from sentio_prober_control.Sentio.CommandGroups.ProbeCommandGroup import ProbeCommandGroup from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp from sentio_prober_control.Sentio.ProberSentio import SentioProber class TestProbeCommandGroup(unittest.TestCase): @@ -19,12 +16,11 @@ def test_async_step_probe_site(self): self.mock_comm.send.assert_called_with("start_step_positioner_site East,1") self.assertEqual(cmd_id, 123) - #----Command is different of document, waiting the fix by another PR--- - # def test_get_probe_site(self): - # self.mock_comm.read_line.return_value = "0,0,site1,1000,2000" - # result = self.test_prober.probe.get_probe_site(ProbeSentio.East, 0) - # self.mock_comm.send.assert_called_with("get_positioner_site East,0") - # self.assertEqual(result, (0, "site1", 1000.0, 2000.0)) + def test_get_probe_site(self): + self.mock_comm.read_line.return_value = "0,0,site1,1000,2000,Home" + result = self.test_prober.probe.get_probe_site(ProbeSentio.East, 0) + self.mock_comm.send.assert_called_with("get_positioner_site East,0") + self.assertEqual(result, ("site1", 1000.0, 2000.0, "Home")) def test_get_probe_xy(self): self.mock_comm.read_line.return_value = "0,0,1500,2500" diff --git a/sentio_prober_control/UnitTest/TestProberSentio.py b/sentio_prober_control/UnitTest/TestProberSentio.py new file mode 100644 index 0000000..3a81988 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestProberSentio.py @@ -0,0 +1,66 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Sentio.Response import Response + +class TestScopeCommandGroup(unittest.TestCase): + def setUp(self): + """Initialize the mock communicator and ScopeCommandGroup instance.""" + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + + # Ensure the mock provides `send` and `read_line` methods + self.test_prober = SentioProber(self.mock_comm) + + def test_get_scope_home(self): + """Test get_scope_home method.""" + self.mock_comm.read_line.return_value = "0,0,250,250" + home_x, home_y = self.test_prober.get_scope_home() + self.mock_comm.send.assert_called_with("get_scope_home") + self.assertEqual(home_x, 250.0) + self.assertEqual(home_y, 250.0) + + def test_set_scope_home_with_values(self): + """Test set_scope_home method with specified X and Y values.""" + self.mock_comm.read_line.return_value = "0,0,OK" + response = self.test_prober.set_scope_home(300, 400) + self.mock_comm.send.assert_called_with("set_scope_home 300,400") + self.assertIsNone(response) # The method does not return a value + + def test_set_scope_home_without_values(self): + """Test set_scope_home method with no arguments (default to current position).""" + self.mock_comm.read_line.return_value = "0,0,OK" + response = self.test_prober.set_scope_home() + self.mock_comm.send.assert_called_with("set_scope_home") + self.assertIsNone(response) + + def test_step_scope_site(self): + """Test step_scope_site method.""" + self.mock_comm.read_line.return_value = "0,0,Pos1,1000,2000" + site_id, offset_x, offset_y = self.test_prober.step_scope_site(2) + self.mock_comm.send.assert_called_with("step_scope_site 2") + self.assertEqual(site_id, "Pos1") + self.assertEqual(offset_x, 1000.0) + self.assertEqual(offset_y, 2000.0) + + def test_step_scope_site_first(self): + """Test step_scope_site_first method.""" + self.mock_comm.read_line.return_value = "0,0,Pos1,0,0" + site_id, offset_x, offset_y = self.test_prober.step_scope_site_first() + self.mock_comm.send.assert_called_with("step_scope_site_first") + self.assertEqual(site_id, "Pos1") + self.assertEqual(offset_x, 0.0) + self.assertEqual(offset_y, 0.0) + + def test_step_scope_site_next(self): + """Test step_scope_site_next method.""" + self.mock_comm.read_line.return_value = "0,0,Pos2,1000,1000" + site_id, offset_x, offset_y = self.test_prober.step_scope_site_next() + self.mock_comm.send.assert_called_with("step_scope_site_next") + self.assertEqual(site_id, "Pos2") + self.assertEqual(offset_x, 1000.0) + self.assertEqual(offset_y, 1000.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestQAlibriaCommandGroup.py b/sentio_prober_control/UnitTest/TestQAlibriaCommandGroup.py new file mode 100644 index 0000000..b0f9aeb --- /dev/null +++ b/sentio_prober_control/UnitTest/TestQAlibriaCommandGroup.py @@ -0,0 +1,128 @@ +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.Response import Response + +# Dummy response class to simulate Response objects from check_resp. +class DummyResponse: + def __init__(self, resp_str): + # Expected format: "error_code,command_id,message" + parts = resp_str.split(",") + if len(parts) >= 3: + self._message = parts[2] + elif len(parts) == 2: + self._message = parts[1] + else: + self._message = "" + def message(self): + return self._message + +# Patch Response.check_resp to return a DummyResponse instance. +Response.check_resp = lambda x: DummyResponse(x) + +# Dummy parent class that contains a 'comm' attribute. +class DummyParent: + def __init__(self, comm): + self.comm = comm + +class TestQAlibriaCommandGroup(unittest.TestCase): + def setUp(self): + # Create a mock communicator based on the TCP/IP communicator. + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + # Wrap the communicator in a dummy parent so that __parent.comm works. + dummy_parent = DummyParent(self.mock_comm) + self.qal = QAlibriaCommandGroup(dummy_parent) + + def test_calibration_execute(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.calibration_execute() + self.mock_comm.send.assert_called_with("qal:calibration_execute") + + def test_calibration_drift_verify(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.calibration_drift_verify("DUT1", True) + self.mock_comm.send.assert_called_with("qal:calibration_drift_verify DUT1,true") + + def test_start_calibration_deprecated(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.start_calibration() + self.mock_comm.send.assert_called_with("qal:calibration_execute") + + def test_verify_calibration_drift_deprecated(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.verify_calibration_drift() + self.mock_comm.send.assert_called_with("qal:calibration_drift_verify") + + def test_verify_calibration_drift_dut_deprecated(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.verify_calibration_drift_dut("DUT2") + self.mock_comm.send.assert_called_with("qal:calibration_drift_verify DUT2") + + def test_set_calibration_drift_probe12_deprecated(self): + # Provide two responses for the two sequential send commands. + self.mock_comm.read_line.side_effect = ["0,0,FirstResponse", "0,0,SecondResponse"] + result = self.qal.set_calibration_drift_probe12() + expected_calls = [ + call("qal:set_dut_network RefDUT,DriftRef,12,false"), + call("qal:set_dut_network RefDUT,Drift,12,false") + ] + self.mock_comm.send.assert_has_calls(expected_calls) + self.assertEqual(result, "SecondResponse") + + def test_get_calibration_status(self): + self.mock_comm.read_line.return_value = "0,0,OK" + status = self.qal.get_calibration_status() + self.mock_comm.send.assert_called_with("qal:get_calibration_status") + self.assertEqual(status, "OK") + + 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) + expected_cmd = "qal:measurement_execute test.snp,1,2,true,false,true" + self.mock_comm.send.assert_called_with(expected_cmd) + + def test_reset_ets(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.reset_ets() + self.mock_comm.send.assert_called_with("qal:reset_ets") + + 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" + self.mock_comm.send.assert_called_with(expected_cmd) + + def test_send_ets_to_vna(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.send_ets_to_vna("cal_set_p12") + expected_cmd = "qal:send_ets_to_vna cal_set_p12" + self.mock_comm.send.assert_called_with(expected_cmd) + + def test_clear_dut_network(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.clear_dut_network("RefDUT", "DriftRef", True) + expected_cmd = "qal:clear_dut_network RefDUT,DriftRef,true" + self.mock_comm.send.assert_called_with(expected_cmd) + + def test_vna_write(self): + self.mock_comm.read_line.return_value = "0,0,OK" + self.qal.vna_write(":SENS1:FREQ:STAR 1.0E9") + expected_cmd = "qal:vna_write :SENS1:FREQ:STAR 1.0E9" + self.mock_comm.send.assert_called_with(expected_cmd) + + def test_vna_query(self): + self.mock_comm.read_line.return_value = "0,0,1.0E9" + result = self.qal.vna_query(":SENS1:FREQ:STAR?") + expected_cmd = "qal:vna_query :SENS1:FREQ:STAR?" + self.mock_comm.send.assert_called_with(expected_cmd) + self.assertEqual(result, "1.0E9") + + def test_vna_read(self): + self.mock_comm.read_line.return_value = "0,0,Data" + result = self.qal.vna_read() + self.mock_comm.send.assert_called_with("qal:vna_read") + self.assertEqual(result, "Data") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/sentio_prober_control/UnitTest/TestSiPHCommanfGroup.py b/sentio_prober_control/UnitTest/TestSiPHCommanfGroup.py new file mode 100644 index 0000000..6517dbe --- /dev/null +++ b/sentio_prober_control/UnitTest/TestSiPHCommanfGroup.py @@ -0,0 +1,122 @@ +import unittest +from unittest.mock import MagicMock +from sentio_prober_control.Communication.CommunicatorTcpIp import CommunicatorTcpIp +from sentio_prober_control.Sentio.Enumerations import ProbeSentio, UvwAxis, FiberType +from sentio_prober_control.Sentio.ProberSentio import SentioProber +from sentio_prober_control.Sentio.Response import Response + +class TestSiPHCommandGroup(unittest.TestCase): + def setUp(self): + """Initialize the mock communicator and SiPHCommandGroup instance.""" + self.mock_comm = MagicMock(spec=CommunicatorTcpIp) + + # Ensure the mock provides `send` and `read_line` methods + self.test_prober = SentioProber(self.mock_comm) + + def test_fast_alignment(self): + """Test fast_alignment method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.fast_alignment() + self.mock_comm.send.assert_called_with("siph:fast_alignment") + + def test_get_cap_sensor(self): + """Test get_cap_sensor method.""" + self.mock_comm.read_line.return_value = "0,0,0.5,1.2" + cap1, cap2 = self.test_prober.siph.get_cap_sensor() + self.mock_comm.send.assert_called_with("siph:get_cap_sensor") + self.assertEqual(cap1, 0.5) + self.assertEqual(cap2, 1.2) + + def test_get_intensity(self): + """Test get_intensity method.""" + self.mock_comm.read_line.return_value = "0,0,1.5" + intensity = self.test_prober.siph.get_intensity(1) + self.mock_comm.send.assert_called_with("siph:get_intensity 1") + self.assertEqual(intensity, 1.5) + + def test_gradient_search(self): + """Test gradient_search method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.gradient_search() + self.mock_comm.send.assert_called_with("siph:gradient_search") + + def test_move_hover(self): + """Test move_hover method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.move_hover(ProbeSentio.East) + self.mock_comm.send.assert_called_with("siph:move_hover East") + + def test_coupling(self): + """Test coupling method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.coupling(ProbeSentio.East, UvwAxis.V) + self.mock_comm.send.assert_called_with("siph:coupling East,V") + + def test_get_alignment(self): + """Test get_alignment method.""" + self.mock_comm.read_line.return_value = "0,0,true,false,true,true" + coarse, fine, gradient, rotary = self.test_prober.siph.get_alignment(ProbeSentio.West, FiberType.Single) + self.mock_comm.send.assert_called_with("siph:get_alignment West,Single") + self.assertTrue(coarse) + self.assertFalse(fine) + self.assertTrue(gradient) + self.assertTrue(rotary) + + def test_set_origin(self): + """Test set_origin method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.set_origin(ProbeSentio.East) + self.mock_comm.send.assert_called_with("siph:set_origin East") + + def test_move_origin(self): + """Test move_origin method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.move_origin(ProbeSentio.West) + self.mock_comm.send.assert_called_with("siph:move_origin West") + + def test_move_position_uvw(self): + """Test move_position_uvw method.""" + self.mock_comm.read_line.return_value = "0,0,0.2" + position = self.test_prober.siph.move_position_uvw(ProbeSentio.East, UvwAxis.U, 0.1) + self.mock_comm.send.assert_called_with("siph:move_position_uvw East,U,0.1") + self.assertEqual(position, 0.2) + + def test_pivot_point(self): + """Test pivot_point method.""" + self.mock_comm.read_line.return_value = "0,0,OK" + self.test_prober.siph.pivot_point(ProbeSentio.West) + self.mock_comm.send.assert_called_with("siph:pivot_point West") + + def test_move_nanocube_xy(self): + """Test move_nanocube_xy method.""" + self.mock_comm.read_line.return_value = "0,0,50.000,50.000" + x, y = self.test_prober.siph.move_nanocube_xy(ProbeSentio.East, 50, 50) + self.mock_comm.send.assert_called_with("siph:move_nanocube_xy East,50,50") + self.assertEqual(x, 50.000) + self.assertEqual(y, 50.000) + + def test_get_nanocube_xy(self): + """Test get_nanocube_xy method.""" + self.mock_comm.read_line.return_value = "0,0,50.000,50.000" + x, y = self.test_prober.siph.get_nanocube_xy(ProbeSentio.East) + self.mock_comm.send.assert_called_with("siph:get_nanocube_xy East") + self.assertEqual(x, 50.000) + self.assertEqual(y, 50.000) + + def test_get_nanocube_z(self): + """Test get_nanocube_z method.""" + self.mock_comm.read_line.return_value = "0,0,50.000" + z = self.test_prober.siph.get_nanocube_z(ProbeSentio.West) + self.mock_comm.send.assert_called_with("siph:get_nanocube_z West") + self.assertEqual(z, 50.000) + + def test_start_tracking(self): + """Test start_tracking method.""" + self.mock_comm.read_line.return_value = "0,5,OK" + command_id = self.test_prober.siph.start_tracking(30) + self.mock_comm.send.assert_called_with("siph:start_tracking 30") + self.assertEqual(command_id, 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/sentio_prober_control/UnitTest/TestStatusCommandGroup.py b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py new file mode 100644 index 0000000..e25c005 --- /dev/null +++ b/sentio_prober_control/UnitTest/TestStatusCommandGroup.py @@ -0,0 +1,101 @@ +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 + + +class TestStatusCommandGroup(unittest.TestCase): + def setUp(self): + self.mock_parent = MagicMock() + self.mock_comm = MagicMock() + self.mock_parent.comm = self.mock_comm + self.status = StatusCommandGroup(self.mock_parent) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_temp(self, mock_resp): + mock_resp.return_value.message.return_value = "30.5,OK" + self.assertEqual(self.status.get_chuck_temp(), 30.5) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_temp_setpoint(self, mock_resp): + mock_resp.return_value.message.return_value = "25.0,OK" + self.assertEqual(self.status.get_chuck_temp_setpoint(), 25.0) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_thermo_state(self, mock_resp): + cases = { + "soaking": ThermoChuckState.Soaking, + "cooling": ThermoChuckState.Cooling, + "heating": ThermoChuckState.Heating, + "controlling": ThermoChuckState.Controlling, + "standby": ThermoChuckState.Standby, + "error": ThermoChuckState.Error, + "uncontrolled": ThermoChuckState.Uncontrolled, + "unknown stuff": ThermoChuckState.Unknown, + } + for msg, expected in cases.items(): + mock_resp.return_value.message.return_value = msg + self.assertEqual(self.status.get_chuck_thermo_state(), expected) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_machine_status(self, mock_resp): + mock_resp.return_value.message.return_value = "0,0,Ready,IsMeasuring,LoaderBusy" + self.assertEqual(self.status.get_machine_status(), (True, True, True)) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_soaking_time(self, mock_resp): + mock_resp.return_value.message.return_value = "120.0" + self.assertEqual(self.status.get_soaking_time(80.0), 120.0) + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_temp(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + resp = self.status.set_chuck_temp(75.0) + self.assertEqual(resp.message(), "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_thermo_energy_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "Fast" + self.assertEqual(self.status.get_chuck_thermo_energy_mode(), "Fast") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_chuck_thermo_hold_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "Active" + self.assertEqual(self.status.get_chuck_thermo_hold_mode(), "Active") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_get_high_purge_state(self, mock_resp): + mock_resp.return_value.message.return_value = "ON" + self.assertEqual(self.status.get_high_purge_state(), "ON") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_thermo_energy_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + resp = self.status.set_chuck_thermo_energy_mode("Optimal") + self.assertEqual(resp.message(), "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_thermo_hold_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + self.status.comm.send = MagicMock() + resp = self.status.set_chuck_thermo_hold_mode(True) + self.status.comm.send.assert_called_with("status:set_chuck_thermo_hold_mode True") + self.assertEqual(resp.message(), "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_chuck_thermo_mode(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + resp = self.status.set_chuck_thermo_mode("Turbo") + self.assertEqual(resp.message(), "OK") + + @patch("sentio_prober_control.Sentio.CommandGroups.StatusCommandGroup.Response.check_resp") + def test_set_high_purge(self, mock_resp): + mock_resp.return_value.message.return_value = "OK" + self.status.comm.send = MagicMock() + resp = self.status.set_high_purge(True) + self.status.comm.send.assert_called_with("status:set_high_purge True") + self.assertEqual(resp.message(), "OK") + + +if __name__ == "__main__": + unittest.main()