Module pipettin-piper.piper.plugins.tools.tool

Functions

def make_tool_param_accessor(controller: Controller, tool_name: str)
Expand source code
def make_tool_param_accessor(controller: Controller, tool_name: str):
    """
    Creates a partial function that accesses parameters of a specific tool in the controller's database.

    NOTE: I'm using "partial" here instead of just making a function because I am afraid of python.

    Args:
        controller (Controller): The controller instance containing the database of tools.
        tool_name (str): The name of the tool whose parameters will be accessed.

    Returns:
        function: A function that takes a key and returns the corresponding parameter value for the specified tool.
                  The function will return a deep copy of the result, meant to block changes to the original data.
    """
    def data_accessor(controller: Controller, tool_name: str):
        return controller.database_tools.getToolByName(tool_name)["parameters"]
    data_accessor = functools.partial(data_accessor, controller=controller, tool_name=tool_name)

    def accessor(key: str, controller: Controller, tool_name: str):
        params = controller.database_tools.getToolByName(tool_name)["parameters"]
        try:
            res = params[key]
        except Exception as e:
            raise KeyError(f"Parameter '{key}' not found in tool '{tool_name}'.") from e
        return deepcopy(res)
    params_accessor = functools.partial(accessor, controller=controller, tool_name=tool_name)

    return data_accessor, params_accessor

Creates a partial function that accesses parameters of a specific tool in the controller's database.

NOTE: I'm using "partial" here instead of just making a function because I am afraid of python.

Args

controller : Controller
The controller instance containing the database of tools.
tool_name : str
The name of the tool whose parameters will be accessed.

Returns

function
A function that takes a key and returns the corresponding parameter value for the specified tool. The function will return a deep copy of the result, meant to block changes to the original data.

Classes

class Tool (tool_data: dict, gcode_builder: GcodeBuilder)
Expand source code
class Tool(TrackedDict):
    """
    Represents a tool in the system, encapsulating its parameters, offsets, and GCODE sequences 
    for operations such as pickup, parking, and activation. A `Tool` object is responsible for 
    generating GCODE commands required to interact with the physical toolhead, ensuring proper 
    tool management and collision avoidance.

    The `Tool` class extends `TrackedDict` to provide dynamic access to tool parameters stored 
    in a database or JSON file, while exposing commonly used offsets and clearances as properties.

    Attributes:
        name (str): The unique name of the tool.
        parameters (dict): Tool parameters as loaded from the database or JSON file.
        parked (bool): Used to track whether the tool is currently parked.
        builder (GcodeBuilder): GCODE builder instance for generating movement commands.
        controller (Controller): Controller instance managing tools and tool data.
        gcode (GcodePrimitives): Object providing basic GCODE commands.
        tool_data (dict): Complete data for the tool, including parameters and metadata.

    Properties:
        tool_offset (dict): Tool offset parameters for the tool's active center.
        x_offset (float): X-axis offset for the tool.
        y_offset (float): Y-axis offset for the tool.
        z_offset (float): Z-axis offset for the tool.
        clearance (dict): Clearance properties to avoid collisions.
        safe_x (float): X clearance added by the tool to the toolhead's sides.
        safe_y (float): Y clearance added by the tool to the toolhead's front.
        safe_y_parked (float): Minimum Y coordinate for the tool when parked.
        safe_y_post (float): Minimum Y coordinate for the tool's empty parking post.
        safe_y_loaded (float): Y coordinate when the tool clears its parking post.
        tool_post (dict): Coordinates and offsets of the tool parking post.

    Notes:
        - The class assumes that tools have parameters like "tool_post" and "docking_commands" 
        for GCODE operations.
        - It ensures safe tool-changing by using predefined clearances and docking sequences 
        to avoid collisions during operations.
    """
    def __init__(self, tool_data: dict, gcode_builder: GcodeBuilder):

        # Save the entry from the "tools" collection in the DB,
        # including parameters and metadata (e.g. name, description).
        # TODO: Disabled "deepcopy" here because tool_data is now a
        #       TrackedDict with an accesor function for data in the DB.
        self.tool_data = tool_data

        # Extract the tool's parameters. These contain the actual tool parameters,
        # which look like the entries in the PIPETTES dict above.
        self.parameters = tool_data["parameters"]

        # Save tool name.
        self.name = tool_data["name"]

        if gcode_builder:
            # Save controller and builder.
            self.builder: GcodeBuilder = gcode_builder

            # Get the gcode generator and main controller.
            self.gcode: GcodePrimitives = self.builder.gcode
            self.controller: Controller = self.builder.controller

            # Setup tool parameter access from the controller's database.
            self.get_data, get_param = make_tool_param_accessor(self.controller, self.name)
            super().__init__(accessor_func=get_param)
        else:
            # Use the default GCODE generator.
            self.gcode = GcodePrimitives()
            # Setup tool parameter access from the provided data.
            super().__init__(data=tool_data["parameters"])
            self.get_data = lambda: self.tool_data


        # NOTE: Tools may register themselves on instantiation in the gcode builder.
        #       Though this is usually the job of a plugin with a "tool loader" method.
        #self.builder.register_tool(self)

    def __repr__(self):
        return f"Tool {self.name}: {self.get_data()}"

    def __iter__(self):
        """
        Returns an iterator over the keys of the dictionary.

        Returns:
            iterator: An iterator over the keys of the dictionary.
        """
        # NOTE: Implementing this method allows updating other dicts with this one.
        # TODO: This may cause issues if the keys are used to update the database in between.
        params = self.get_data()
        return iter(params.keys())

    name: str= None
    """Unique name for the tool."""

    parameters: dict= None
    """Tool parameters as loaded from the DB or JSON file."""

    parked: bool
    """Indicates wether the tool is parked or not."""

    @property
    def tool_offset(self):
        """Tool offset parameters for the active center of the tool."""
        return deepcopy(self["tool_offset"])

    @property
    def x_offset(self):
        """X offset for the tool."""
        x = float(self.tool_offset["x"])
        return x

    @property
    def y_offset(self):
        """Y offset for the tool."""
        y = float(self.tool_offset["y"])
        return y

    @property
    def z_offset(self):
        """Z offset for the tool."""
        z = float(self.tool_offset["z"])
        return z

    @property
    def safe_y(self):
        """Distance along the Y direction that this tool adds to the front of the toolhead.
        Used to avoid collisions with the machine, parked tools, and such."""
        return self.clearance["safe_y"]

    @property
    def safe_x(self):
        """Distance along the X direction that this tool adds to the sides of the toolhead.
        Used to avoid collisions with the machine, parked tools, and such. Use the largest
        value if the tool is not symmetric."""
        return self.clearance["safe_x"]

    @property
    def clearance(self) -> dict:
        """Clearance properties for the tool."""
        return deepcopy(self["clearance"])

    @property
    def safe_y_parked(self):
        """Minimum Y coordinate at which this tool is found when properly parked.
        Used to avoid collisions with the machine, parked tools, and such.
        """
        # NOTE: The post might protrude further than the tool itself.
        #       Use the minimum of the two, if available.
        safe_y_post = self.safe_y_post
        safe_y_parked = self.clearance["safe_y_parked"]

        # Choose the most conservative clearance.
        if safe_y_post is not None and safe_y_parked is not None:
            # NOTE: Corrected for axis inversion.
            safe_y_parked = max(safe_y_parked, safe_y_post)

        return safe_y_parked

    @property
    def safe_y_post(self):
        """Minimum Y coordinate at which the parking post is found when empty.
        This will tipically depend on the length of pins in the parking post.
        Used to avoid collisions with the machine, parked tools, and such.
        """
        return self.clearance["safe_y_post"]

    @property
    def safe_y_loaded(self):
        """Y coordinate at which the tool is barely clear of its (empty) parking post.
        Derived from "safe_y" and "safe_y_post". Useful for toolchanging.
        """
        # NOTE: Corrected for axis inversion.
        return self.safe_y_post + self.safe_y

    @property
    def tool_post(self):
        """Coordinates of the tool-post.
        Example values:
        - x: 16.50,
        - y: 280,
        - z: 19
        - y_docking_closeup: 2
        - z_docking_offset: 0.0
        - y_parking_closeup: 4
        - z_parking_offset: 1.0
        - feedrate: 500
        """
        return deepcopy(self["tool_post"])

    # TODO: Add "active center" size, meant to avoid collisions with
    #       workspace objects due to bulky tools at their active site.
    # tool_shape: dict = {"size": 10, "x": 10, "y": 2, "diameter": 10}

    def pickup(self, old_tool, new_tool) -> list:
        """Pickup a tool.
        Assumes an empty toolhead, and a tool in its parking post.
        """
        commands = [self.gcode.comment(f"Picking up {self.name}.")]

        # Home tool axis.
        # NOTE: Homing should be overriden by child classes.
        #       See the "Pipette" class.

        # Home the toolchanger remote.
        # TODO: "gcodeHomeP" is largely deprecated. Replace it.
        commands += self.gcode.gcodeHomeP(cmd_prefix="HOME_", which_axis="E0")

        # Generate commands for the pickup.
        commands += self.gcode_dock()

        # Mark the tool as not parked.
        self.parked = False

        # Mark a toolchange / activate the new tool.
        commands += self.gcode_activate()

        return commands

    def gcode_activate(self) -> list:
        """Signal the firmware to activate this tool.
        This usually means sending gcode commands equivalent to T0, T1, etc. which may switch the active extruder.
        The need for it is really up to the firmware and the machine.
        Example:
        - "activate": "T1"
        """
        return deepcopy(self["activate"])

    def gcode_deactivate(self) -> list:
        """Signal the firmware to deactivate this tool.
        Example:
        - "deactivate": "; dummy deactivate."
        """
        return deepcopy(self["deactivate"])

    def park(self, old_tool, new_tool) -> list:
        """Park a tool.
        Assumes that the tool is mounted, and that its parking post is empty.
        """
        # Generate commands for the pickup.
        parking_commands = self.gcode_park()

        # Mark the tool as parked.
        self.parked = True

        # TODO: Is a "deactivate" function needed for the pipettes?
        parking_commands += self.gcode_deactivate()

        return parking_commands

    @property
    def docking_commands(self):
        """GCODE command list for the docking sequence
        This command is run after placing the empty toolhead at the parking post's coordinates.
        It starts at those coordinates and must end with a correctly docked tool.
        Example:
        >>> [
        >>>     "T0",
        >>>     "G91",
        >>>     "M83",
        >>>     "G1 Y-1.0 E-1.0 F100",
        >>>     "G1 Y-1.0 E-3.0 F50",
        >>>     "G1 E-2.0 F50"
        >>> ]
        """
        # Return a copy of the commands.
        return deepcopy(self["docking_commands"])

    def gcode_post_front(self, commands: list = None) -> list:
        """Generate GCODE to approack a parking post (assuming none is loaded)."""

        if commands is None:
            commands = []

        # Initialize commands list.
        commands += [self.gcode.comment("Moving in front of parking post.")]

        # Clearance moves.
        commands += self.gcode_safe_zy()

        # Parked tool coordinates.
        x, _, z = self.tool_post["x"], self.tool_post["y"], self.tool_post["z"]
        # Extra Z-axis distance used to help align the tool.
        z_offset = self.tool_post["z_docking_offset"]

        # Initial approach.
        commands += self.gcode_approach(x, self.safe_y_parked, z+z_offset)

        # Done.
        commands += [self.gcode.comment("Done moving in front of parking post.")]

        return commands

    # Custom GCODE section
    def gcode_dock(self, commands: list = None) -> list:
        """Generate GCODE to dock a tool (assuming none is loaded).

        Required tool parameters:
        - tool_post: coordinates and offsets of the tool post.
        - docking_commands: list of GCODE commands.
        - parking_commands: list of GCODE commands.

        Returns:
            list: List of GCODES for docking.
        """

        if commands is None:
            commands = []

        # Initialize commands list.
        commands += [self.gcode.comment("START tool-change pickup macro.")]

        # Clearance moves.
        commands += self.gcode_safe_zy()

        # Parked tool coordinates.
        x, y, z = self.tool_post["x"], self.tool_post["y"], self.tool_post["z"]
        # Extra Y-axis distance needed to approach and touch the tool.
        y_docking_closeup = self.tool_post["y_docking_closeup"]
        # Extra Z-axis distance used to help align the tool.
        z_offset = self.tool_post["z_docking_offset"]

        # Initial approach.
        commands += self.gcode_approach(x, self.safe_y_parked, z+z_offset)

        # Closeup move.
        commands += self.gcode_approach(x, y+y_docking_closeup, z+z_offset)

        # Final approach moves.
        commands += self.gcode_align(y=y, z=z+z_offset)

        # Tool-changer commands to lock the tool to the toolhead.
        commands += [self.gcode.comment("Locking moves.")]
        commands += self.docking_commands

        # End sequence.
        commands += self.gcode.G0(
            y=self.safe_y_loaded,
            absolute=True,
            add_comment="Move back to the Y coordinate clear of other parked tools.")

        # Done.
        commands += [self.gcode.comment("END tool-change macro.")]

        return commands

    @property
    def parking_commands(self):
        """GCODE command list for the undocking sequence
        This command is run after placing the tool at the parking post's coordinates.
        It starts at those coordinates and must end with a free and correctly parked tool.
        Example:
        >>> [
        >>>     "T0",
        >>>     "G91",
        >>>     "M83",
        >>>     "G1 Y4 E6 F100",
        >>>     "G1 Y5 F500"
        >>> ]
        """
        # Return a copy of the commands.
        return deepcopy(self["parking_commands"])

    def gcode_park(self, commands: list = None) -> list:
        """Generate GCODE to park a tool (assuming none is loaded).
        TODO: Document this function. See 'gcode_dock' for deatils for now.
        """
        if commands is None:
            commands = []

        # Initialize commands list.
        commands += [self.gcode.comment("START tool-change parking macro.")]

        # Clearance moves.
        commands += self.gcode_safe_zy()

        # Parking post coordinates.
        x, y, z = self.tool_post["x"], self.tool_post["y"], self.tool_post["z"]
        # Extra Y-axis distance needed to approach the post and hang the tool.
        y_parking_closeup = self.tool_post["y_parking_closeup"]
        # Extra Z-axis distance used to help align the tool.
        z_offset = self.tool_post["z_parking_offset"]

        # TODO: This clearance move may be trivial. Consider removing.
        commands += self.gcode_approach(x, self.safe_y_loaded, z+z_offset)

        # Initial approach.
        commands += self.gcode_approach(x, y, z+z_offset)

        # Final approach moves.
        # NOTE: Corrected for axis inversion.
        commands += self.gcode_align(y=y-y_parking_closeup, z=z+z_offset)

        # Tool-changer commands to release the tool from the toolhead.
        commands += [self.gcode.comment("Unlocking moves.")]
        commands += self.parking_commands

        # End sequence.
        commands += self.gcode.G0(
            y=self.safe_y_parked,
            absolute=True,
            add_comment="Move back to the Y coordinate clear of other parked tools.")

        # Done.
        commands += [self.gcode.comment("END tool-change macro.")]

        return commands

    def gcode_safe_zy(self, commands: list = None) -> list:
        """Make G1 GCODE commands to move to safe Z and Y coordinates, in that order."""
        # Use a new command list if none was provided.
        if commands is None:
            commands = []
        # Get the absolute clearance coordinates
        safe_z = self.builder.getSafeHeight()
        safe_y = self.builder.getSafeY()

        # Clearance moves.
        commands += [self.gcode.comment("Safety clearance moves."),
                    "G90" + self.gcode.comment("Set absolute motion mode.")]
        commands += self.gcode.gcodeClearance(
            z=safe_z,
            f=self.builder.feedrate,
            add_comment="Move to the safe Z height.")
        commands += self.gcode.G0(
            y=safe_y,
            add_comment="Move to the Y coordinate clear of other parked tools.")

        return commands

    def gcode_approach(self, x, y, z) -> list:
        """Make G1 GCODE to move in front of a tool-post.
        Moves first in X, then in YZ.
        """
        # Start sequence: approach the tool.
        commands = [self.gcode.comment("Alignment moves.")]
        commands += self.gcode.G0(
            x=x,
            add_comment="Move to the X position in front of the parking post.")
        commands += self.gcode.G0(
            y=y,
            z=z,
            add_comment="Move to the Y-Z position in front of the parking post/tool.")

        return commands

    def gcode_align(self, y, z) -> list:
        """Make G1 GCODE for fine alignment to a tool-post.
        Moves first in Y (fine and slow approach) and then in Z (wobble compensation).
        """
        # Slow feedrates for the moves.
        tc_feedrate = self.tool_post["feedrate"]

        # Initial docking moves.
        commands = [self.gcode.comment("Alignment moves.")]
        commands += self.gcode.G1(
            y=y,
            f=tc_feedrate,
            add_comment="Partial approach for initial alignment.")
        commands += self.gcode.G1(
            z=z,
            f=tc_feedrate,
            add_comment="Compensate Z for looseness in the parking post.")

        return commands

    @property
    def homing(self):
        """Homing parameters and commands for the pipette.
        Example: {
            "commands": [
            "T2",
            "HOME_EXTRUDER EXTRUDER=extruder2",
            "G90",
            "M82",
            "G1 E0 F6000; Total free motion minus 'travel_distance' below."
            ],
            "commands.desc": "Homing commands, leaving the pipette at the maximum volume.",
            "travel_distance": 30,
            "travel_distance.desc": "Free travel distance after homing, to the lowest volume position."
        }
        """
        return deepcopy(self["homing"])

    def home(self) -> list:
        """Generate GCODE commands needed to home this tool.
        Then update the internal state accordingly.
        """

        logging.warning(f"Placeholder homing routine for tool '{self.name}'.")

        # Build the command
        if not self.controller.machine.dry:
            logging.info(f"Homing tool '{self.name}'.")
            homing_commands = self.homing['commands']
        else:
            # Dry mode:
            msg = f"Simulating homed state for tool '{self.name}'. Dry mode enabled."
            logging.info(msg)
            homing_commands = [self.gcode.comment(msg)]

        return homing_commands

Represents a tool in the system, encapsulating its parameters, offsets, and GCODE sequences for operations such as pickup, parking, and activation. A Tool object is responsible for generating GCODE commands required to interact with the physical toolhead, ensuring proper tool management and collision avoidance.

The Tool class extends TrackedDict to provide dynamic access to tool parameters stored in a database or JSON file, while exposing commonly used offsets and clearances as properties.

Attributes

name : str
The unique name of the tool.
parameters : dict
Tool parameters as loaded from the database or JSON file.
parked : bool
Used to track whether the tool is currently parked.
builder : GcodeBuilder
GCODE builder instance for generating movement commands.
controller : Controller
Controller instance managing tools and tool data.
gcode : GcodePrimitives
Object providing basic GCODE commands.
tool_data : dict
Complete data for the tool, including parameters and metadata.

Properties

tool_offset (dict): Tool offset parameters for the tool's active center. x_offset (float): X-axis offset for the tool. y_offset (float): Y-axis offset for the tool. z_offset (float): Z-axis offset for the tool. clearance (dict): Clearance properties to avoid collisions. safe_x (float): X clearance added by the tool to the toolhead's sides. safe_y (float): Y clearance added by the tool to the toolhead's front. safe_y_parked (float): Minimum Y coordinate for the tool when parked. safe_y_post (float): Minimum Y coordinate for the tool's empty parking post. safe_y_loaded (float): Y coordinate when the tool clears its parking post. tool_post (dict): Coordinates and offsets of the tool parking post.

Notes

  • The class assumes that tools have parameters like "tool_post" and "docking_commands" for GCODE operations.
  • It ensures safe tool-changing by using predefined clearances and docking sequences to avoid collisions during operations.

Initializes the TrackedDict instance.

Args

file_path : str
The path to the YAML file.
data : dict
Altertative (and sufficient) data source. Used to update the YAML file if any.

Ancestors

  • TrackedDict
  • YAMLReader
  • collections.UserDict
  • collections.abc.MutableMapping
  • collections.abc.Mapping
  • collections.abc.Collection
  • collections.abc.Sized
  • collections.abc.Iterable
  • collections.abc.Container

Subclasses

Class variables

var name : str

Unique name for the tool.

var parameters : dict

Tool parameters as loaded from the DB or JSON file.

var parked : bool

Indicates wether the tool is parked or not.

Instance variables

prop clearance : dict
Expand source code
@property
def clearance(self) -> dict:
    """Clearance properties for the tool."""
    return deepcopy(self["clearance"])

Clearance properties for the tool.

prop docking_commands
Expand source code
@property
def docking_commands(self):
    """GCODE command list for the docking sequence
    This command is run after placing the empty toolhead at the parking post's coordinates.
    It starts at those coordinates and must end with a correctly docked tool.
    Example:
    >>> [
    >>>     "T0",
    >>>     "G91",
    >>>     "M83",
    >>>     "G1 Y-1.0 E-1.0 F100",
    >>>     "G1 Y-1.0 E-3.0 F50",
    >>>     "G1 E-2.0 F50"
    >>> ]
    """
    # Return a copy of the commands.
    return deepcopy(self["docking_commands"])

GCODE command list for the docking sequence This command is run after placing the empty toolhead at the parking post's coordinates. It starts at those coordinates and must end with a correctly docked tool. Example:

>>> [
>>>     "T0",
>>>     "G91",
>>>     "M83",
>>>     "G1 Y-1.0 E-1.0 F100",
>>>     "G1 Y-1.0 E-3.0 F50",
>>>     "G1 E-2.0 F50"
>>> ]
prop homing
Expand source code
@property
def homing(self):
    """Homing parameters and commands for the pipette.
    Example: {
        "commands": [
        "T2",
        "HOME_EXTRUDER EXTRUDER=extruder2",
        "G90",
        "M82",
        "G1 E0 F6000; Total free motion minus 'travel_distance' below."
        ],
        "commands.desc": "Homing commands, leaving the pipette at the maximum volume.",
        "travel_distance": 30,
        "travel_distance.desc": "Free travel distance after homing, to the lowest volume position."
    }
    """
    return deepcopy(self["homing"])

Homing parameters and commands for the pipette. Example: { "commands": [ "T2", "HOME_EXTRUDER EXTRUDER=extruder2", "G90", "M82", "G1 E0 F6000; Total free motion minus 'travel_distance' below." ], "commands.desc": "Homing commands, leaving the pipette at the maximum volume.", "travel_distance": 30, "travel_distance.desc": "Free travel distance after homing, to the lowest volume position." }

prop parking_commands
Expand source code
@property
def parking_commands(self):
    """GCODE command list for the undocking sequence
    This command is run after placing the tool at the parking post's coordinates.
    It starts at those coordinates and must end with a free and correctly parked tool.
    Example:
    >>> [
    >>>     "T0",
    >>>     "G91",
    >>>     "M83",
    >>>     "G1 Y4 E6 F100",
    >>>     "G1 Y5 F500"
    >>> ]
    """
    # Return a copy of the commands.
    return deepcopy(self["parking_commands"])

GCODE command list for the undocking sequence This command is run after placing the tool at the parking post's coordinates. It starts at those coordinates and must end with a free and correctly parked tool. Example:

>>> [
>>>     "T0",
>>>     "G91",
>>>     "M83",
>>>     "G1 Y4 E6 F100",
>>>     "G1 Y5 F500"
>>> ]
prop safe_x
Expand source code
@property
def safe_x(self):
    """Distance along the X direction that this tool adds to the sides of the toolhead.
    Used to avoid collisions with the machine, parked tools, and such. Use the largest
    value if the tool is not symmetric."""
    return self.clearance["safe_x"]

Distance along the X direction that this tool adds to the sides of the toolhead. Used to avoid collisions with the machine, parked tools, and such. Use the largest value if the tool is not symmetric.

prop safe_y
Expand source code
@property
def safe_y(self):
    """Distance along the Y direction that this tool adds to the front of the toolhead.
    Used to avoid collisions with the machine, parked tools, and such."""
    return self.clearance["safe_y"]

Distance along the Y direction that this tool adds to the front of the toolhead. Used to avoid collisions with the machine, parked tools, and such.

prop safe_y_loaded
Expand source code
@property
def safe_y_loaded(self):
    """Y coordinate at which the tool is barely clear of its (empty) parking post.
    Derived from "safe_y" and "safe_y_post". Useful for toolchanging.
    """
    # NOTE: Corrected for axis inversion.
    return self.safe_y_post + self.safe_y

Y coordinate at which the tool is barely clear of its (empty) parking post. Derived from "safe_y" and "safe_y_post". Useful for toolchanging.

prop safe_y_parked
Expand source code
@property
def safe_y_parked(self):
    """Minimum Y coordinate at which this tool is found when properly parked.
    Used to avoid collisions with the machine, parked tools, and such.
    """
    # NOTE: The post might protrude further than the tool itself.
    #       Use the minimum of the two, if available.
    safe_y_post = self.safe_y_post
    safe_y_parked = self.clearance["safe_y_parked"]

    # Choose the most conservative clearance.
    if safe_y_post is not None and safe_y_parked is not None:
        # NOTE: Corrected for axis inversion.
        safe_y_parked = max(safe_y_parked, safe_y_post)

    return safe_y_parked

Minimum Y coordinate at which this tool is found when properly parked. Used to avoid collisions with the machine, parked tools, and such.

prop safe_y_post
Expand source code
@property
def safe_y_post(self):
    """Minimum Y coordinate at which the parking post is found when empty.
    This will tipically depend on the length of pins in the parking post.
    Used to avoid collisions with the machine, parked tools, and such.
    """
    return self.clearance["safe_y_post"]

Minimum Y coordinate at which the parking post is found when empty. This will tipically depend on the length of pins in the parking post. Used to avoid collisions with the machine, parked tools, and such.

prop tool_offset
Expand source code
@property
def tool_offset(self):
    """Tool offset parameters for the active center of the tool."""
    return deepcopy(self["tool_offset"])

Tool offset parameters for the active center of the tool.

prop tool_post
Expand source code
@property
def tool_post(self):
    """Coordinates of the tool-post.
    Example values:
    - x: 16.50,
    - y: 280,
    - z: 19
    - y_docking_closeup: 2
    - z_docking_offset: 0.0
    - y_parking_closeup: 4
    - z_parking_offset: 1.0
    - feedrate: 500
    """
    return deepcopy(self["tool_post"])

Coordinates of the tool-post. Example values: - x: 16.50, - y: 280, - z: 19 - y_docking_closeup: 2 - z_docking_offset: 0.0 - y_parking_closeup: 4 - z_parking_offset: 1.0 - feedrate: 500

prop x_offset
Expand source code
@property
def x_offset(self):
    """X offset for the tool."""
    x = float(self.tool_offset["x"])
    return x

X offset for the tool.

prop y_offset
Expand source code
@property
def y_offset(self):
    """Y offset for the tool."""
    y = float(self.tool_offset["y"])
    return y

Y offset for the tool.

prop z_offset
Expand source code
@property
def z_offset(self):
    """Z offset for the tool."""
    z = float(self.tool_offset["z"])
    return z

Z offset for the tool.

Methods

def gcode_activate(self) ‑> list
Expand source code
def gcode_activate(self) -> list:
    """Signal the firmware to activate this tool.
    This usually means sending gcode commands equivalent to T0, T1, etc. which may switch the active extruder.
    The need for it is really up to the firmware and the machine.
    Example:
    - "activate": "T1"
    """
    return deepcopy(self["activate"])

Signal the firmware to activate this tool. This usually means sending gcode commands equivalent to T0, T1, etc. which may switch the active extruder. The need for it is really up to the firmware and the machine. Example: - "activate": "T1"

def gcode_align(self, y, z) ‑> list
Expand source code
def gcode_align(self, y, z) -> list:
    """Make G1 GCODE for fine alignment to a tool-post.
    Moves first in Y (fine and slow approach) and then in Z (wobble compensation).
    """
    # Slow feedrates for the moves.
    tc_feedrate = self.tool_post["feedrate"]

    # Initial docking moves.
    commands = [self.gcode.comment("Alignment moves.")]
    commands += self.gcode.G1(
        y=y,
        f=tc_feedrate,
        add_comment="Partial approach for initial alignment.")
    commands += self.gcode.G1(
        z=z,
        f=tc_feedrate,
        add_comment="Compensate Z for looseness in the parking post.")

    return commands

Make G1 GCODE for fine alignment to a tool-post. Moves first in Y (fine and slow approach) and then in Z (wobble compensation).

def gcode_approach(self, x, y, z) ‑> list
Expand source code
def gcode_approach(self, x, y, z) -> list:
    """Make G1 GCODE to move in front of a tool-post.
    Moves first in X, then in YZ.
    """
    # Start sequence: approach the tool.
    commands = [self.gcode.comment("Alignment moves.")]
    commands += self.gcode.G0(
        x=x,
        add_comment="Move to the X position in front of the parking post.")
    commands += self.gcode.G0(
        y=y,
        z=z,
        add_comment="Move to the Y-Z position in front of the parking post/tool.")

    return commands

Make G1 GCODE to move in front of a tool-post. Moves first in X, then in YZ.

def gcode_deactivate(self) ‑> list
Expand source code
def gcode_deactivate(self) -> list:
    """Signal the firmware to deactivate this tool.
    Example:
    - "deactivate": "; dummy deactivate."
    """
    return deepcopy(self["deactivate"])

Signal the firmware to deactivate this tool. Example: - "deactivate": "; dummy deactivate."

def gcode_dock(self, commands: list = None) ‑> list
Expand source code
def gcode_dock(self, commands: list = None) -> list:
    """Generate GCODE to dock a tool (assuming none is loaded).

    Required tool parameters:
    - tool_post: coordinates and offsets of the tool post.
    - docking_commands: list of GCODE commands.
    - parking_commands: list of GCODE commands.

    Returns:
        list: List of GCODES for docking.
    """

    if commands is None:
        commands = []

    # Initialize commands list.
    commands += [self.gcode.comment("START tool-change pickup macro.")]

    # Clearance moves.
    commands += self.gcode_safe_zy()

    # Parked tool coordinates.
    x, y, z = self.tool_post["x"], self.tool_post["y"], self.tool_post["z"]
    # Extra Y-axis distance needed to approach and touch the tool.
    y_docking_closeup = self.tool_post["y_docking_closeup"]
    # Extra Z-axis distance used to help align the tool.
    z_offset = self.tool_post["z_docking_offset"]

    # Initial approach.
    commands += self.gcode_approach(x, self.safe_y_parked, z+z_offset)

    # Closeup move.
    commands += self.gcode_approach(x, y+y_docking_closeup, z+z_offset)

    # Final approach moves.
    commands += self.gcode_align(y=y, z=z+z_offset)

    # Tool-changer commands to lock the tool to the toolhead.
    commands += [self.gcode.comment("Locking moves.")]
    commands += self.docking_commands

    # End sequence.
    commands += self.gcode.G0(
        y=self.safe_y_loaded,
        absolute=True,
        add_comment="Move back to the Y coordinate clear of other parked tools.")

    # Done.
    commands += [self.gcode.comment("END tool-change macro.")]

    return commands

Generate GCODE to dock a tool (assuming none is loaded).

Required tool parameters: - tool_post: coordinates and offsets of the tool post. - docking_commands: list of GCODE commands. - parking_commands: list of GCODE commands.

Returns

list
List of GCODES for docking.
def gcode_park(self, commands: list = None) ‑> list
Expand source code
def gcode_park(self, commands: list = None) -> list:
    """Generate GCODE to park a tool (assuming none is loaded).
    TODO: Document this function. See 'gcode_dock' for deatils for now.
    """
    if commands is None:
        commands = []

    # Initialize commands list.
    commands += [self.gcode.comment("START tool-change parking macro.")]

    # Clearance moves.
    commands += self.gcode_safe_zy()

    # Parking post coordinates.
    x, y, z = self.tool_post["x"], self.tool_post["y"], self.tool_post["z"]
    # Extra Y-axis distance needed to approach the post and hang the tool.
    y_parking_closeup = self.tool_post["y_parking_closeup"]
    # Extra Z-axis distance used to help align the tool.
    z_offset = self.tool_post["z_parking_offset"]

    # TODO: This clearance move may be trivial. Consider removing.
    commands += self.gcode_approach(x, self.safe_y_loaded, z+z_offset)

    # Initial approach.
    commands += self.gcode_approach(x, y, z+z_offset)

    # Final approach moves.
    # NOTE: Corrected for axis inversion.
    commands += self.gcode_align(y=y-y_parking_closeup, z=z+z_offset)

    # Tool-changer commands to release the tool from the toolhead.
    commands += [self.gcode.comment("Unlocking moves.")]
    commands += self.parking_commands

    # End sequence.
    commands += self.gcode.G0(
        y=self.safe_y_parked,
        absolute=True,
        add_comment="Move back to the Y coordinate clear of other parked tools.")

    # Done.
    commands += [self.gcode.comment("END tool-change macro.")]

    return commands

Generate GCODE to park a tool (assuming none is loaded). TODO: Document this function. See 'gcode_dock' for deatils for now.

def gcode_post_front(self, commands: list = None) ‑> list
Expand source code
def gcode_post_front(self, commands: list = None) -> list:
    """Generate GCODE to approack a parking post (assuming none is loaded)."""

    if commands is None:
        commands = []

    # Initialize commands list.
    commands += [self.gcode.comment("Moving in front of parking post.")]

    # Clearance moves.
    commands += self.gcode_safe_zy()

    # Parked tool coordinates.
    x, _, z = self.tool_post["x"], self.tool_post["y"], self.tool_post["z"]
    # Extra Z-axis distance used to help align the tool.
    z_offset = self.tool_post["z_docking_offset"]

    # Initial approach.
    commands += self.gcode_approach(x, self.safe_y_parked, z+z_offset)

    # Done.
    commands += [self.gcode.comment("Done moving in front of parking post.")]

    return commands

Generate GCODE to approack a parking post (assuming none is loaded).

def gcode_safe_zy(self, commands: list = None) ‑> list
Expand source code
def gcode_safe_zy(self, commands: list = None) -> list:
    """Make G1 GCODE commands to move to safe Z and Y coordinates, in that order."""
    # Use a new command list if none was provided.
    if commands is None:
        commands = []
    # Get the absolute clearance coordinates
    safe_z = self.builder.getSafeHeight()
    safe_y = self.builder.getSafeY()

    # Clearance moves.
    commands += [self.gcode.comment("Safety clearance moves."),
                "G90" + self.gcode.comment("Set absolute motion mode.")]
    commands += self.gcode.gcodeClearance(
        z=safe_z,
        f=self.builder.feedrate,
        add_comment="Move to the safe Z height.")
    commands += self.gcode.G0(
        y=safe_y,
        add_comment="Move to the Y coordinate clear of other parked tools.")

    return commands

Make G1 GCODE commands to move to safe Z and Y coordinates, in that order.

def home(self) ‑> list
Expand source code
def home(self) -> list:
    """Generate GCODE commands needed to home this tool.
    Then update the internal state accordingly.
    """

    logging.warning(f"Placeholder homing routine for tool '{self.name}'.")

    # Build the command
    if not self.controller.machine.dry:
        logging.info(f"Homing tool '{self.name}'.")
        homing_commands = self.homing['commands']
    else:
        # Dry mode:
        msg = f"Simulating homed state for tool '{self.name}'. Dry mode enabled."
        logging.info(msg)
        homing_commands = [self.gcode.comment(msg)]

    return homing_commands

Generate GCODE commands needed to home this tool. Then update the internal state accordingly.

def park(self, old_tool, new_tool) ‑> list
Expand source code
def park(self, old_tool, new_tool) -> list:
    """Park a tool.
    Assumes that the tool is mounted, and that its parking post is empty.
    """
    # Generate commands for the pickup.
    parking_commands = self.gcode_park()

    # Mark the tool as parked.
    self.parked = True

    # TODO: Is a "deactivate" function needed for the pipettes?
    parking_commands += self.gcode_deactivate()

    return parking_commands

Park a tool. Assumes that the tool is mounted, and that its parking post is empty.

def pickup(self, old_tool, new_tool) ‑> list
Expand source code
def pickup(self, old_tool, new_tool) -> list:
    """Pickup a tool.
    Assumes an empty toolhead, and a tool in its parking post.
    """
    commands = [self.gcode.comment(f"Picking up {self.name}.")]

    # Home tool axis.
    # NOTE: Homing should be overriden by child classes.
    #       See the "Pipette" class.

    # Home the toolchanger remote.
    # TODO: "gcodeHomeP" is largely deprecated. Replace it.
    commands += self.gcode.gcodeHomeP(cmd_prefix="HOME_", which_axis="E0")

    # Generate commands for the pickup.
    commands += self.gcode_dock()

    # Mark the tool as not parked.
    self.parked = False

    # Mark a toolchange / activate the new tool.
    commands += self.gcode_activate()

    return commands

Pickup a tool. Assumes an empty toolhead, and a tool in its parking post.

Inherited members