@final
class Ump4Binding(BaseBinding):
"""Bindings for UMP-4 platform"""
def __init__(self) -> None:
"""Initialize UMP-4 bindings."""
# Establish connection to Sensapex API (exit if connection fails).
UMP.set_library_path(RESOURCES_DIRECTORY)
self._ump = UMP.get_ump() # pyright: ignore [reportUnknownMemberType]
@staticmethod
@override
def get_display_name() -> str:
return "Sensapex uMp-4"
@staticmethod
@override
def get_cli_name() -> str:
return "ump-4"
@override
async def get_manipulators(self) -> list[str]:
return list(map(str, self._ump.list_devices()))
@override
async def get_axes_count(self) -> int:
return 4
@override
def get_dimensions(self) -> Vector4:
return Vector4(x=20, y=20, z=20, w=20)
@override
async def get_position(self, manipulator_id: str) -> Vector4:
return um_to_mm(list_to_vector4(self._get_device(manipulator_id).get_pos(1))) # pyright: ignore [reportUnknownMemberType]
@override
async def get_angles(self, manipulator_id: str) -> NoReturn:
"""uMp-4 does not support getting angles so raise an error.
Raises:
AttributeError: uMp-4 does not support getting angles.
"""
error_message = "UMP-4 does not support getting angles"
raise AttributeError(error_message)
@override
async def get_shank_count(self, manipulator_id: str) -> NoReturn:
"""uMp-4 does not support getting shank count so raise an error.
Raises:
AttributeError: uMp-4 does not support getting shank count.
"""
error_message = "UMP-4 does not support getting shank count"
raise AttributeError(error_message)
@override
def get_movement_tolerance(self) -> float:
return 0.001
@override
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
# Convert position to micrometers.
target_position_um = vector_mm_to_um(position)
# Request movement.
movement = self._get_device(manipulator_id).goto_pos( # pyright: ignore [reportUnknownMemberType]
vector4_to_array(target_position_um), scalar_mm_to_um(speed)
)
# Wait for movement to finish.
_ = await get_running_loop().run_in_executor(None, movement.finished_event.wait, None)
# Handle interrupted movement.
if movement.interrupted:
error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}" # pyright: ignore [reportUnknownMemberType]
raise RuntimeError(error_message)
# Handle empty end position.
if not movement.last_pos: # pyright: ignore [reportUnknownMemberType]
error_message = f"Manipulator {manipulator_id} did not reach target position"
raise RuntimeError(error_message)
return um_to_mm(list_to_vector4(movement.last_pos)) # pyright: ignore [reportArgumentType, reportUnknownMemberType]
@override
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
# Augment current position with depth.
current_position = await self.get_position(manipulator_id)
new_platform_position = current_position.model_copy(update={"w": depth})
# Make the movement.
final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
# Return the final depth.
return float(final_platform_position.w)
@override
async def stop(self, manipulator_id: str) -> None:
self._get_device(manipulator_id).stop()
@override
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
# unified <- platform
# +x <- +y
# +y <- -z
# +z <- +x
# +d <- +d
return Vector4(
x=platform_space.y,
y=self.get_dimensions().z - platform_space.z,
z=platform_space.x,
w=platform_space.w,
)
@override
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
# platform <- unified
# +x <- +z
# +y <- +x
# +z <- -y
# +d <- +d
return Vector4(
x=unified_space.z,
y=unified_space.x,
z=self.get_dimensions().z - unified_space.y,
w=unified_space.w,
)
# Helper methods.
def _get_device(self, manipulator_id: str) -> SensapexDevice:
return self._ump.get_device(int(manipulator_id)) # pyright: ignore [reportUnknownMemberType]