"""Franka gripper interface via panda_py.
High-level interface for controlling gripper for Franka Emika Panda / FR3 robots via panda_py.
Copyright (c) 2024 Jozef Stefan Institute
Authors: Leon Zlajpah
"""
try:
from panda_py import libfranka # pyright: ignore[reportMissingImports]
except Exception as e:
raise ModuleNotFoundError(f"{e}\nPython bindings for the Panda not installed. \nYou can install them through pip:\n pip install panda-python") from None
from typing import Any, Optional
from time import sleep
from robotblockset.grippers import gripper
from robotblockset.robots import robot
from robotblockset.tools import isscalar
[docs]
class panda_gripper(gripper):
"""PandaPy-backed interface for controlling the Franka Emika Panda gripper.
Attributes
----------
Name : str
Identifier string for the gripper instance.
Robot : robot or None
Robot instance this gripper is attached to, if available.
hostname : str
Hostname or IP address of the Panda/FR3 controller used by the gripper.
"""
[docs]
def __init__(self, robot: Optional[robot], name: str = "panda_gripper", hostname: Optional[str] = None, **kwargs: Any) -> None:
"""
Initializes the gripper object with default attributes.
Parameters
----------
robot : robot, optional
robot, optional
An instance of the robot class that the gripper is attached to.
name : str, optional
Name identifier for the gripper instance (default is 'panda_gripper').
hostname : str, optional
IP address or hostname of the Panda / FR3 robot. Has to be defined if no robot is selected.
**kwargs : Any
Additional keyword arguments for future extensions or configuration.
Returns
-------
None
This constructor initializes the Panda gripper interface in place.
Raises
------
ValueError
If neither a robot nor a non-empty hostname is provided.
"""
gripper.__init__(self, **kwargs)
self.Name = name
self.Robot = robot
if robot is None:
if not isinstance(hostname, str) or not hostname.strip():
raise ValueError("A non-empty hostname is required when robot is None")
self.hostname = hostname
else:
self.hostname = robot.hostname
self.gripper = libfranka.Gripper(self.hostname)
self.gripper_state = self.gripper.read_once()
self._width_grasp = 0
self._width = self.gripper_state.width
self._width_max = self.gripper_state.max_width
self._state = -1
self._speed = 0.0
self._speed_max = 0.5
self._force_max = 70.0
self._verbose = 1
self.DebugMessage("Created")
@property
def width(self) -> float:
"""
Get the current width of the gripper.
Returns
-------
float
Current gripper width.
"""
self.gripper_state = self.gripper.read_once()
self._width = self.gripper_state.width
return self.gripper_state.width
[docs]
def is_grasped(self) -> bool:
"""
Check if the gripper is currently grasping an object.
Returns
-------
bool
True if the gripper is grasping an object, False otherwise.
"""
self.gripper_state = self.gripper.read_once()
return self.gripper_state.is_grasped
[docs]
def GetState(self) -> str:
"""
Returns the current state of the gripper.
Returns
-------
str
The state of the gripper, either "Opened", "Closed", or "Undefined".
"""
self.gripper_state = self.gripper.read_once()
if self.gripper_state.is_grasped:
self._state = 1
return "Closed"
else:
self._state = 0
return "Opened"
[docs]
def Grasp(self, width: float, speed: float = 0.1, force: float = 5, eps: float = 0.005, **kwargs: Any) -> bool:
"""
Grasps an object with the gripper at a specified width.
Parameters
----------
width : float
The width to which the gripper should close to grasp the object.
speed : float, optional
The speed at which the gripper should move (default is 0.1).
force : float, optional
The force to apply during the grasp (default is 5).
eps : float, optional
The tolerance for the grasping width (default is 0.005).
**kwargs : dict
Additional arguments for the grasp operation, may include a "width" key.
Returns
-------
bool
True if the gripper successfully grasps, False otherwise.
Raises
------
ValueError
If width, speed, force, or epsilon is outside the supported range.
"""
width, speed = self._validate_width_speed(width, speed)
if not isscalar(force) or force <= 0 or force > self._force_max:
raise ValueError(f"Force must be in the range (0, {self._force_max}]")
if not isscalar(eps) or eps < 0 or eps > self._width_max:
raise ValueError(f"Epsilon must be in the range [0, {self._width_max}]")
force = float(force)
eps = float(eps)
_success = self.gripper.grasp(width, speed, force, epsilon_inner=eps, epsilon_outer=eps)
if _success:
self._state = 1
else:
self._state = -1
return _success
[docs]
def Move(self, width: float, speed: float = 0.1) -> bool:
"""
Moves the gripper to a specified width.
Parameters
----------
width : float
The width to which the gripper should move.
speed : float, optional
The speed at which the gripper should move (default is 0.1).
Returns
-------
bool
True if the gripper successfully moves, False otherwise.
Raises
------
ValueError
If width or speed is outside the supported range.
"""
width, speed = self._validate_width_speed(width, speed)
self._state = -1
return self.gripper.move(width, speed)
def _validate_width_speed(self, width: float, speed: float) -> tuple[float, float]:
"""Validate and normalize a gripper motion target."""
if not isscalar(width) or width < 0 or width > self._width_max:
raise ValueError(f"Width must be in the range [0, {self._width_max}]")
if not isscalar(speed) or speed <= 0 or speed > self._speed_max:
raise ValueError(f"Speed must be in the range (0, {self._speed_max}]")
return float(width), float(speed)
[docs]
def Homing(self) -> bool:
"""
Resets the gripper to an open state.
Returns
-------
bool
True if the gripper successfully homes, False otherwise.
"""
self._state = 0
sleep(1) # Wait for the gripper to settle before homing
return self.gripper.homing()