diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py index e8bc37da959..a8fc32ca142 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py @@ -7,6 +7,9 @@ from . import ( test_connectivity, + test_z_axis, + test_x_axis, + test_l_axis, ) @@ -14,6 +17,9 @@ class TestSection(enum.Enum): """Test Section.""" CONNECTIVITY = "CONNECTIVITY" + Z_AXIS = "Z_AXIS" + L_AXIS = "L_AXIS" + X_AXIS = "X_AXIS" @dataclass @@ -29,6 +35,18 @@ class TestConfig: TestSection.CONNECTIVITY, test_connectivity.run, ), + ( + TestSection.Z_AXIS, + test_z_axis.run, + ), + ( + TestSection.L_AXIS, + test_l_axis.run, + ), + ( + TestSection.X_AXIS, + test_x_axis.run, + ), ] @@ -40,6 +58,18 @@ def build_report(test_name: str) -> CSVReport: CSVSection( title=TestSection.CONNECTIVITY.value, lines=test_connectivity.build_csv_lines(), - ) + ), + CSVSection( + title=TestSection.Z_AXIS.value, + lines=test_z_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.L_AXIS.value, + lines=test_l_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.X_AXIS.value, + lines=test_x_axis.build_csv_lines(), + ), ], ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py index 04d833fa8a5..3005405e61b 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py @@ -26,6 +26,53 @@ class StackerInfo: sn: str +class StackerAxis(Enum): + """Stacker Axis.""" + + X = "X" + Z = "Z" + L = "L" + + def __str__(self) -> str: + """Name.""" + return self.name + + +class Direction(Enum): + """Direction.""" + + RETRACT = 0 + EXTENT = 1 + + def __str__(self) -> str: + """Convert to tag for clear logging.""" + return "negative" if self == Direction.RETRACT else "positive" + + def opposite(self) -> "Direction": + """Get opposite direction.""" + return Direction.EXTENT if self == Direction.RETRACT else Direction.RETRACT + + def distance(self, distance: float) -> float: + """Get signed distance, where retract direction is negative.""" + return distance * -1 if self == Direction.RETRACT else distance + + +@dataclass +class MoveParams: + """Move Parameters.""" + + max_speed: float | None = None + acceleration: float | None = None + max_speed_discont: float | None = None + + def __str__(self) -> str: + """Convert to string.""" + v = "V:" + str(self.max_speed) if self.max_speed else "" + a = "A:" + str(self.acceleration) if self.acceleration else "" + d = "D:" + str(self.max_speed_discont) if self.max_speed_discont else "" + return f"{v} {a} {d}".strip() + + class FlexStacker: """FLEX Stacker Driver.""" @@ -87,6 +134,66 @@ def set_serial_number(self, sn: str) -> None: return self._send_and_recv(f"M996 {sn}\n", "M996 OK") + def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status. + + :return: True if limit switch is triggered, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M119 .*{axis.name}{direction.name[0]}:(\d) .* OK\n") + res = self._send_and_recv("M119\n", "M119 XE:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for limit switch: {res}" + return bool(int(match.group(1))) + + def get_platform_sensor(self, direction: Direction) -> bool: + """Get platform sensor status. + + :return: True if platform is present, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M121 .*{direction.name[0]}:(\d) .* OK\n") + res = self._send_and_recv("M121\n", "M119 E:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for platform sensor: {res}" + return bool(int(match.group(1))) + + def get_hopper_door_closed(self) -> bool: + """Get whether or not door is closed. + + :return: True if door is closed, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(r"^M122 (\d) OK\n") + res = self._send_and_recv("M122\n", "M122 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for hopper door switch: {res}" + return bool(int(match.group(1))) + + def move_in_mm( + self, axis: StackerAxis, distance: float, params: MoveParams | None = None + ) -> None: + """Move axis.""" + if self._simulating: + return + self._send_and_recv(f"G0 {axis.name}{distance} {params or ''}\n", "G0 OK") + + def move_to_limit_switch( + self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None + ) -> None: + """Move until limit switch is triggered.""" + if self._simulating: + return + self._send_and_recv( + f"G5 {axis.name}{direction.value} {params or ''}\n", "G0 OK" + ) + def __del__(self) -> None: """Close serial port.""" if not self._simulating: diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py new file mode 100644 index 00000000000..d892bdc1fd7 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py @@ -0,0 +1,70 @@ +"""Test L Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction + + +class LimitSwitchError(Exception): + """Limit Switch Error.""" + + pass + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("trigger-latch-switch", [CSVResult]), + CSVLine("release/open-latch", [CSVResult]), + CSVLine("hold/close-latch", [CSVResult]), + ] + + +def get_latch_held_switch(driver: FlexStacker) -> bool: + """Get limit switch.""" + held_switch = driver.get_limit_switch(StackerAxis.L, Direction.RETRACT) + print("(Held Switch triggered) : ", held_switch) + return held_switch + + +def close_latch(driver: FlexStacker) -> None: + """Close latch.""" + driver.move_to_limit_switch(StackerAxis.L, Direction.EXTENT) + + +def open_latch(driver: FlexStacker) -> None: + """Open latch.""" + driver.move_in_mm(StackerAxis.L, -22) + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not get_latch_held_switch(driver): + print("Switch is not triggered, try to trigger it by closing latch...") + close_latch(driver) + if not get_latch_held_switch(driver): + print("!!! Held switch is still not triggered !!!") + report(section, "trigger-latch-switch", [CSVResult.FAIL]) + return + + report(section, "trigger-latch-switch", [CSVResult.PASS]) + + ui.print_header("Latch Release/Open") + open_latch(driver) + success = not get_latch_held_switch(driver) + report(section, "release/open-latch", [CSVResult.from_bool(success)]) + + ui.print_header("Latch Hold/Close") + if not success: + print("Latch must be open to close it") + report(section, "hold/close-latch", [CSVResult.FAIL]) + else: + close_latch(driver) + success = get_latch_held_switch(driver) + report(section, "hold/close-latch", [CSVResult.from_bool(success)]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py new file mode 100644 index 00000000000..802c12bcae5 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py @@ -0,0 +1,81 @@ +"""Test X Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + CSVLine( + "platform-sensor-trigger-positive-untrigger-negative", + [bool, bool, CSVResult], + ), + CSVLine( + "platform-sensor-trigger-negative-untrigger-positive", + [bool, bool, CSVResult], + ), + ] + + +def test_platform_sensors_for_direction( + driver: FlexStacker, direction: Direction, report: CSVReport, section: str +) -> None: + """Test platform sensors for a given direction.""" + ui.print_header(f"Platform Sensor - {direction} direction") + sensor_result = driver.get_platform_sensor(direction) + opposite_result = not driver.get_platform_sensor(direction.opposite()) + print(f"{direction} sensor triggered: {sensor_result}") + print(f"{direction.opposite()} sensor untriggered: {opposite_result}") + report( + section, + f"platform-sensor-trigger-{direction}-untrigger-{direction.opposite()}", + [ + sensor_result, + opposite_result, + CSVResult.from_bool(sensor_result and opposite_result), + ], + ) + + +def platform_is_removed(driver: FlexStacker) -> bool: + """Check if the platform is removed from the carrier.""" + plus_side = driver.get_platform_sensor(Direction.EXTENT) + minus_side = driver.get_platform_sensor(Direction.RETRACT) + return not plus_side and not minus_side + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not driver._simulating and not platform_is_removed(driver): + print("FAILURE - Cannot start tests with platform on the carrier") + return + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.EXTENT, report, section + ) + + if not driver._simulating: + ui.get_user_ready("Place the platform on the X carrier") + + test_platform_sensors_for_direction(driver, Direction.EXTENT, report, section) + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.RETRACT, report, section + ) + + test_platform_sensors_for_direction(driver, Direction.RETRACT, report, section) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py new file mode 100644 index 00000000000..58fc733e0dc --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py @@ -0,0 +1,34 @@ +"""Test Z Axis.""" +from typing import List, Union +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + ] + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.EXTENT, report, section + ) + + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.RETRACT, report, section + ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py new file mode 100644 index 00000000000..2aca90c8886 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py @@ -0,0 +1,38 @@ +"""Utility functions for the Flex Stacker EVT QC module.""" +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction, MoveParams + + +def test_limit_switches_per_direction( + driver: FlexStacker, + axis: StackerAxis, + direction: Direction, + report: CSVReport, + section: str, + speed: float = 50.0, +) -> None: + """Sequence to test the limit switch for one direction.""" + ui.print_header(f"{axis} Limit Switch - {direction} direction") + # first make sure switch is not already triggered by moving in the opposite direction + if driver.get_limit_switch(axis, direction): + print(f"{direction} switch already triggered, moving away...\n") + SAFE_DISTANCE_MM = 10 + driver.move_in_mm(axis, direction.opposite().distance(SAFE_DISTANCE_MM)) + + # move until the limit switch is reached + print(f"moving towards {direction} limit switch...\n") + driver.move_to_limit_switch(axis, direction, MoveParams(max_speed=speed)) + result = driver.get_limit_switch(axis, direction) + opposite_result = not driver.get_limit_switch(axis, direction.opposite()) + print(f"{direction} switch triggered: {result}") + print(f"{direction.opposite()} switch untriggered: {opposite_result}") + report( + section, + f"limit-switch-trigger-{direction}-untrigger-{direction.opposite()}", + [result, opposite_result, CSVResult.from_bool(result and opposite_result)], + )