def check_plate_registration_points(self, plate_index: int):
        """Move to each teach point for the deck plate."""
        REG_POINT = ["Bottom Left", "Bottom Right", "Upper Right"]

        if plate_index < 0 or plate_index >= self.__class__.DECK_PLATE_COUNT:
            raise UserInputError(f"Error: deck plates must fall \
                within the range: [0, {plate_index}).")

        if self.active_tool_index != self.__class__.CAMERA_TOOL_INDEX:
            self.pickup_tool(self.__class__.CAMERA_TOOL_INDEX)

        if self.position[2] < self.safe_z:
            self.move_xyz_absolute(z=self.safe_z)

        try:
            self.enable_live_video()
            for index, coords in enumerate(self.deck_config['plates'][str(
                    plate_index)]['corner_well_centroids']):
                if coords is None or coords[0] is None or coords[1] is None:
                    raise UserInputError(
                        f"Error: this reference position for deck plate {plate_index} is not defined."
                    )
                self.move_xy_absolute(coords[0], coords[1], wait=True)
                # TODO: adjust focus??
                # TODO: let user implement adjustments in this situation?
                self.input(
                    f"Currently positioned at index: {REG_POINT[index]} | {coords}. Press any key to continue."
                )
        finally:
            self.disable_live_video()
        self.park_tool()
    def sonicate_well(self,
                      deck_index: int,
                      row_letter: str,
                      column_index: int,
                      plunge_depth: float,
                      seconds: float,
                      power: float,
                      pulse_duty_cycle: float,
                      pulse_interval: float,
                      autoclean: bool = True):
        """Sonicate one well at a specified depth for a given time. Then clean the tip.
            deck_index: deck index where the plate lives
            row_letter: row coordinate to sonicate at
            column_index: number coordinate to sonicate at
            plunge_depth: depth (in mm) to plunge from the top of the plate.
            seconds: time (in sec) to sonicate for
            power: sonicator power level ranging from 0.4 (default, min) through 1.0 (max).
            autoclean: whether or not to perform the predefined autoclean routine.

            Note: sonicator does not turn on below power level of 0.4.
        """

        # Json dicts enforce that keys must be strings.
        deck_index_str = str(deck_index)

        if deck_index_str not in self.deck_config['plates']:
            raise UserInputError(
                f"Error: deck plate {deck_index} is not configured.")

        plate_height = self.deck_config['plates'][deck_index_str][
            'plate_height']
        plunge_height = plate_height - plunge_depth
        # Sanity check that we're not plunging too deep. Plunge depth is relative.
        if plunge_height < 0:
            raise UserInputError("Error: plunge depth is too deep.")

        if self.active_tool_index != self.__class__.SONICATOR_TOOL_INDEX:
            self.pickup_tool(self.__class__.SONICATOR_TOOL_INDEX)

        row_index = ord(row_letter.upper()) - 65  # convert letters to numbers.
        column_index -= 1  # Convert 1-indexed plates to 0-indexing.
        x, y = self._get_well_position(deck_index, row_index, column_index)

        print(
            f"Moving to: ({x:.3f}, {y:.3f}) | {row_letter}{column_index + 1}")
        self.move_xy_absolute(x, y)  # Position over the well at safe z height.
        self.move_xyz_absolute(z=plunge_height, wait=True)
        print(f"Sonicating for {seconds} seconds!!")
        self.sonicator.sonicate(seconds, power, pulse_duty_cycle,
                                pulse_interval)
        print("done!")
        self.move_xy_absolute()  # leave the machine at the safe height.
        if autoclean:
            self.clean_sonicator()
 def execute_protocol(self, protocol):
     """Execute a list of protocol commands."""
     for cmd in protocol:
         if cmd['operation'] not in self.protocol_methods:
             raise UserInputError(
                 f"Error. Method cmd['name'] is not a method that can be used in a protocol."
             )
         fn = self.protocol_methods[cmd['operation']]
         kwargs = cmd['specs']
         kwargs['self'] = self
         fn(**kwargs)
    def idle_z(self, z: float = None):
        """Set the specified height to be the \"idle z\" height.
        If no height is specified, the machine will take the current height.
        """
        if z is None:
            # Get current height.
            _, _, z = self.position

        if z < 0:
            raise UserInputError("Error: idle_z value cannot be under zero.")

        max_z_height = self.axis_limits[2][1]  # [Z axis][max limit]
        # Duet specifies tool z offsets as negative, so we want the most negative one (min).
        max_tool_z_offset = min(self.tool_z_offsets)
        max_idle_z = max_z_height + max_tool_z_offset
        if z > max_idle_z:
            raise UserInputError(f"Error: Cannot set idle_z height to {z}mm. " \
                f"The tallest tool restricts maximum height above the bed to {max_idle_z}mm.")

        self.deck_config['idle_z'] = z
 def save_deck_config(self, file_path: str = None):
     """Save the current configuration of plates on the deck to a file.
     If no filepath is specified, save from the initial config file specified on instantiation.
     If no filepath is specified and no initial config file was specified.
     """
     self.check_config()
     if file_path is None and self.deck_config_filepath is None:
         raise UserInputError(
             "Error: no file path is specified from which to save the deck configuration."
         )
     if file_path is None:
         file_path = self.deck_config_filepath
     with open(file_path, 'w+') as config_file:
         json.dump(self.deck_config, config_file, indent=4)
         print(f"Saving configuration to {file_path}.")
         # Update the save location so we default to saving the file we loaded from.
         file_path = self.deck_config_filepath
 def load_deck_config(self, file_path: str = None):
     """Load a configuration of plates on the deck from the specified file path.
     If no file path is specified, reload from initial config file specified on instantiation.
     If no file path is specified and no initial config file was specified, error out.
     """
     if file_path is None and self.deck_config_filepath is None:
         raise UserInputError(
             "Error: no file path is specified to load the deck configuration from."
         )
     if file_path is None:
         # This is effectively a "reload."
         file_path = self.deck_config_filepath
         print(
             f"Reloading deck configuration. Overriding any unsaved configuration changes."
         )
     with open(file_path, 'r') as config_file:
         print(f"Loading deck configuration from {file_path}.")
         self.deck_config = json.loads(config_file.read())
         # Update the load location so we default to saving the file we loaded from.
         file_path = self.deck_config_filepath
     self.check_config()